JVM 原理|java知识点
JVM 内存区域
JVM 运行时数据区可以分为线程共享和线程私有两类。
线程共享区域
- 堆(Heap):存放对象实例,是垃圾收集器管理的主要区域。几乎所有对象实例和数组都在堆上分配内存。
- 方法区 / 元空间(Metaspace):存放类信息、常量、静态变量、即时编译器编译后的代码缓存等。JDK 1.8 之后永久代被元空间替代,元空间使用本地内存(Native Memory),不再受 JVM 堆大小限制。
- 直接内存(Direct Memory):不属于 JVM 运行时数据区,通过 NIO 的
ByteBuffer.allocateDirect()分配,由操作系统直接管理。不受-Xmx限制,但受物理内存和MaxDirectMemorySize约束。
线程私有区域
- 虚拟机栈(VM Stack):每个方法执行时创建一个栈帧,存放局部变量表、操作数栈、动态链接、方法出口等信息。方法执行完毕后栈帧弹出。线程结束时栈也随之销毁。
- 本地方法栈(Native Method Stack):为 Native 方法服务,HotSpot 虚拟机将本地方法栈和虚拟机栈合二为一。
- 程序计数器(Program Counter Register):记录当前线程正在执行的字节码行号。如果执行的是 Native 方法,则计数器值为空。此区域是唯一不会发生
OutOfMemoryError的区域。
对象创建与内存布局
对象创建过程
- 类加载检查:虚拟机遇到
new指令时,先检查常量池中是否有该类的符号引用,并检查该类是否已加载、解析和初始化。若未加载,则先执行类加载。 - 分配内存:根据堆是否规整,选择指针碰撞(Bump the Pointer)或空闲列表(Free List)方式分配。堆是否规整取决于垃圾收集器是否带有压缩整理功能。
- 初始化零值:将分配到的内存空间初始化为零值(不包括对象头),保证对象的实例字段可以在不赋初值的情况下使用。
- 设置对象头:在对象头中存储哈希码、GC 分代年龄、锁状态标志、类元数据指针等信息。
- 执行
<init>方法:执行构造方法,完成对象的初始化。
对象内存布局
对象在堆中的存储布局分为三个部分:
- 对象头(Object Header):包含 Mark Word(哈希码、GC 分代年龄、锁状态)和类型指针(指向类元数据,JVM 通过此指针确定对象属于哪个类)。
- 实例数据(Instance Data):对象真正存储的有效信息,即代码中定义的各种类型的字段值。分配顺序受分配策略影响,相同宽度的字段总是被分配到一起。
- 对齐填充(Padding):HotSpot 要求对象起始地址必须是 8 字节的整数倍,不足时通过填充对齐。
垃圾收集
如何判断对象已死亡
- 引用计数法:给对象添加一个引用计数器,有引用加 1,引用失效减 1。实现简单,但无法解决循环引用问题。JVM 不采用此方法。
- 可达性分析算法:从一组称为 GC Roots 的根对象出发,沿引用链向下搜索。不可达的对象即为可回收对象。JVM 默认使用此算法。
可作为 GC Roots 的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等。
引用类型
- 强引用(Strong Reference):最常见的引用,如
Object obj = new Object()。只要强引用存在,垃圾收集器永远不会回收被引用的对象。 - 软引用(Soft Reference):描述还有用但非必需的对象。在内存溢出之前,会将这些对象列入回收范围进行二次回收。通过
SoftReference实现。 - 弱引用(Weak Reference):只能生存到下一次垃圾收集之前。无论内存是否足够,都会回收弱引用关联的对象。通过
WeakReference实现。 - 虚引用(Phantom Reference):最弱的引用,无法通过虚引用获取对象实例。唯一目的是在对象被回收时收到一个系统通知。通过
PhantomReference实现。
垃圾收集算法
- 标记-清除(Mark-Sweep):先标记可回收对象,再统一清除。缺点是产生大量不连续的内存碎片,且效率不稳定。
- 复制算法(Copying):将内存分为两块,每次只使用其中一块。存活对象复制到另一块后,清理当前块。实现简单、效率高,但浪费一半内存空间。新生代中 Eden 与 Survivor 的比例默认为 8,通过复制算法实现 Minor GC。
- 标记-整理(Mark-Compact):标记后将存活对象向一端移动,然后清理边界以外的内存。适用于老年代。
- 分代收集(Generational Collection):根据对象存活周期将堆分为新生代和老年代。新生代中每次收集有大批对象死去,选用复制算法;老年代中对象存活率高,选用标记-清除或标记-整理算法。
对象晋升与大对象
- 对象在 Eden 区出生,经过一次 Minor GC 后存活并能被 Survivor 容纳,将进入 Survivor 区,对象年龄设为 1。每经过一次 Minor GC 并存活,年龄加 1。
- 当年龄达到阈值(默认 15,可通过
-XX:MaxTenuringThreshold设置),对象晋升到老年代。 - 大对象(需要大量连续内存空间的对象,如长字符串和大数组)直接进入老年代,避免在 Eden 和 Survivor 之间来回复制。
类加载机制
类加载过程
类的生命周期包括:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载。其中验证、准备、解析统称为连接。
- 加载:通过类的全限定名获取定义该类的二进制字节流,将其转化为方法区的运行时数据结构,在堆中生成对应的
Class对象。 - 验证:确保字节流符合 JVM 规范,包括文件格式验证、元数据验证、字节码验证和符号引用验证。
- 准备:为类的静态变量分配内存并设置零值(
static final常量直接赋值)。 - 解析:将常量池中的符号引用替换为直接引用。
- 初始化:执行类构造器
<clinit>方法,即执行静态变量赋值和静态代码块。
类加载器
- Bootstrap ClassLoader(启动类加载器):加载
JAVA_HOME/lib下的核心类库,由 C++ 实现,是 JVM 的一部分。没有对应的 Java 类。 - Extension ClassLoader(扩展类加载器):加载
JAVA_HOME/lib/ext下的扩展类库。 - Application ClassLoader(应用程序类加载器):加载用户类路径(ClassPath)上的类,是程序中默认的类加载器。
双亲委派模型
除了启动类加载器外,每个类加载器都有一个父加载器。当一个类加载器收到类加载请求时,首先不会自己去尝试加载,而是把请求委派给父加载器。只有当父加载器无法完成加载时,子加载器才会尝试自己加载。
双亲委派的优势:
- 保证核心类库不被篡改(如自定义
java.lang.String不会被加载) - 避免同一个类被不同的类加载器重复加载
- 保证 Java 程序的稳定运行
自定义类加载器时,遵循双亲委派只需重写 findClass();要打破双亲委派则需要重写 loadClass() 方法。
常用 JVM 参数
| 参数 | 含义 |
|---|---|
-Xms | 堆初始大小 |
-Xmx | 堆最大大小 |
-Xmn | 新生代大小 |
-Xss | 每个线程的栈大小 |
-XX:MetaspaceSize | 元空间初始大小 |
-XX:MaxMetaspaceSize | 元空间最大大小 |
-XX:MaxTenuringThreshold | 晋升老年代的年龄阈值 |
-XX:+PrintGCDetails | 打印 GC 详细日志 |
本文内容整理自 JVM 学习笔记,涵盖内存区域、对象创建、垃圾收集、类加载与常用参数等核心知识点。
