Java对象生命周期
一个Java对象从被加载到JVM内存,到最终被垃圾回收器(GC)清除,会经历一个完整的生命周期,主要包含以下几个关键阶段:
一、 类的加载与初始化(发生在对象创建之前)
- 加载:
- 当JVM第一次遇到
new
关键字、静态字段访问、静态方法调用、反射调用(如Class.forName()
)、子类初始化触发父类初始化等场景时,如果目标类尚未被加载,JVM会通过类加载器查找并加载该类的字节码(.class
文件)到内存中。
- 当JVM第一次遇到
- 验证:
- 确保被加载的字节码是符合JVM规范的、安全的,不会危害虚拟机自身。检查内容包括文件格式、元数据、字节码语义、符号引用等。
- 准备:
- 为类的静态变量分配内存(在方法区)并设置其初始零值(
0
,null
,false
等)。final static
常量如果其值在编译期可知,通常在这个阶段就直接赋值了。
- 为类的静态变量分配内存(在方法区)并设置其初始零值(
- 解析:
- 将常量池内的符号引用(以文本形式表示的引用,如类名、方法名、字段名)替换为直接引用(指向内存中具体位置的指针、偏移量或句柄)。解析可能在初始化之前或之后进行。
- 初始化:
- 执行类的初始化方法
<clinit>()
,该方法由编译器自动收集类中所有静态变量赋值语句和静态代码块合并生成。 - 此时,静态变量被赋予程序中定义的初始值。
- 执行类的初始化方法
二、 对象的创建与初始化
- 内存分配:
- 当遇到
new
指令时,JVM 首先在堆内存中为新生对象分配内存空间。分配方式取决于使用的垃圾收集器和堆内存状态:- 指针碰撞: 堆内存规整时,移动指针划分空间。
- 空闲列表: 堆内存不规整时,从空闲列表中找足够大的空间。
- TLAB: 为每个线程在 Eden 区预先分配一小块私有内存(Thread Local Allocation Buffer),优先在 TLAB 中分配,避免同步开销,提升效率。如果 TLAB 用完或对象太大,再采用上述两种方式。
- 当遇到
- 内存空间初始化:
- 将分配到的内存空间(不包括对象头)初始化为零值(
0
,null
,false
等)。这确保了对象的实例字段在不显式初始化时也有默认值。
- 将分配到的内存空间(不包括对象头)初始化为零值(
- 设置对象头:
- 在对象起始位置设置对象头信息,通常包括:
- Mark Word: 存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这是实现锁(synchronized)、GC 分代年龄记录等机制的关键。
- 类型指针: 指向该对象所属类元数据(
Klass
)在方法区的指针,JVM 通过它确定对象是哪个类的实例。 - 数组长度: 如果对象是数组,对象头还会包含数组长度信息。
- 在对象起始位置设置对象头信息,通常包括:
- 执行实例构造器:
- 调用对象的实例构造器
<init>()
方法(由编译器根据源代码中的构造函数和字段初始化语句生成)。 - 按照程序员的意图,执行字段的显式初始化(如
int a = 10;
)和构造函数中的代码。 - 此时,对象才真正完成了创建过程,处于一个可用的状态。
- 调用对象的实例构造器
三、 对象的使用
- 对象创建完成后,程序就可以通过栈上的引用(reference variable)来操作堆中的这个对象实例了。
- 引用可以存储在局部变量表、操作数栈、类的静态变量或实例变量中。
- 对象在其可达期间(即存在至少一条从 GC Roots 出发的引用链能访问到该对象)会一直被使用。
四、 对象的不可达与垃圾回收判定
- 成为垃圾:
- 当对象不再被程序中的任何地方引用时(即没有任何 GC Roots 能通过引用链访问到该对象),它就变成了垃圾。
- GC Roots 包括:虚拟机栈(栈帧中的局部变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI(即 Native 方法)引用的对象、JVM 内部的引用(如基本类型对应的 Class 对象,常驻异常对象 NullPointerException/OutOfMemoryError,系统类加载器等)、被同步锁(synchronized 关键字)持有的对象。
- 垃圾回收判定算法(判断对象是否存活):
- 可达性分析: 主流的 JVM 使用可达性分析算法。从 GC Roots 集合出发,沿着引用链(Reference Chain)向下搜索。如果一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的。
- 引用类型: Java 提供了不同强度的引用类型,影响 GC 行为:
- 强引用:
Object obj = new Object();
只要强引用存在,对象就绝不会被回收。 - 软引用:
SoftReference
在内存不足即将发生 OOM 之前,会被垃圾回收器回收。常用于缓存。 - 弱引用:
WeakReference
在下一次垃圾回收发生时,无论内存是否充足,都会被回收。常用于实现规范映射(如WeakHashMap
)。 - 虚引用:
PhantomReference
最弱,无法通过它访问对象。主要用于在对象被回收时收到一个系统通知(通过ReferenceQueue
)。
- 强引用:
五、 垃圾回收与内存清除
- 标记:
- 垃圾收集器会标记出所有存活的对象。通常分为两步:
- 初始标记: 暂停所有用户线程(STW - Stop The World),只标记与 GC Roots 直接关联的对象。速度很快。
- 并发标记 / 最终标记: 在并发标记阶段(如 CMS, G1),用户线程与 GC 线程并发执行,从初始标记的对象出发,遍历整个对象图,标记所有可达对象。最终标记阶段(或重新标记阶段)会再次短暂 STW,处理并发标记期间引用发生变化的对象(使用 Card Table 等技术记录这些变化)。
- 垃圾收集器会标记出所有存活的对象。通常分为两步:
- 清除 / 回收:
- 根据不同的垃圾收集算法,对未被标记(即死亡)的对象进行处理:
- 标记-清除: 直接清除死亡对象占用的内存。简单但会产生内存碎片。
- 标记-复制: 将存活的对象复制到另一个预留的内存区域(如 Survivor 区),然后清除原区域的所有内存。解决碎片问题,但空间利用率低(需要预留一半空间)。常用于新生代(Young Generation)的 Minor GC。
- 标记-整理: 让所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。解决碎片问题,且空间利用率高,但移动对象成本较高。常用于老年代(Old/Tenured Generation)的 Full GC/Major GC。
- 分代收集: 现代 JVM 通常结合使用不同算法。将堆划分为新生代(大量对象朝生夕死)和老年代(存活时间长)。新生代使用高效的复制算法(Minor GC)。老年代使用标记-清除或标记-整理算法(Major GC/Full GC)。G1 等收集器采用了不同的分区模型。
- 根据不同的垃圾收集算法,对未被标记(即死亡)的对象进行处理:
- Finalization(可选且不推荐依赖):
- 在垃圾收集器真正回收一个对象的内存之前,如果发现该对象覆盖了
finalize()
方法且该方法尚未被调用过,会将该对象放入一个名为F-Queue
的队列中。 - 由一条低优先级的 JVM 线程(Finalizer 线程)去异步地执行这些对象的
finalize()
方法。 - 重要警告:
finalize()
执行时机不确定,甚至可能根本不执行(如程序提前退出)。- 在
finalize()
中,对象有可能“复活”自己(通过重新建立与 GC Roots 的引用链),但这会阻止本次回收,且下次 GC 时不会再调用finalize()
。 finalize()
执行慢会拖慢F-Queue
,可能导致内存回收延迟甚至 OOM。- 强烈建议避免使用
finalize()
方法! 释放资源应使用try-with-resources
(实现AutoCloseable
接口)或显式调用close()
等方法。
- 在垃圾收集器真正回收一个对象的内存之前,如果发现该对象覆盖了
- 内存清除:
- 在清除/回收阶段,垃圾收集器会将死亡对象占用的内存空间标记为可用。
- 这块内存空间就可以用于分配给后续新创建的对象了。
- 对于标记-复制和标记-整理算法,清除过程隐含在对象移动或区域重置中。对于标记-清除,需要在清除阶段显式释放。
理解这个生命周期对于编写高效、内存安全的 Java 程序至关重要,尤其是在涉及内存泄漏调优、GC 性能优化时。记住,finalize()
是最后的、不可靠的逃生舱,永远不要依赖它来释放关键资源。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论