
对象什么时候算“出生”:new 之后不等于立刻可用
执行 new 操作只是触发了内存分配和构造函数调用,但对象真正“活过来”的前提是构造函数完整返回——中间任何一步抛出异常(比如 NullPointerException 或自定义异常),JVM 都会中止初始化,已分配的内存可能立即被丢弃,不会进入后续生命周期阶段。
常见错误现象:在构造函数里启动线程、注册监听器或调用 this 引用的方法,结果其他线程拿到的是未初始化完成的对象,出现字段为 null 或默认值(如 0、false)。
- 避免在构造函数中暴露
this(比如传给静态方法、启动线程、加入集合) - 若必须延迟初始化,改用工厂方法 + 显式
init(),而非依赖构造函数“做完一切” - 注意
final字段的语义:它只保证构造结束时可见性,不阻止构造中途被其他线程看到未完成状态
对象什么时候开始“可被回收”:不是没引用就立刻回收
GC 判断对象是否存活,看的是“从 GC Roots 是否可达”,不是简单检查变量是否还持有引用。局部变量早超出作用域、集合里删掉了元素、甚至把引用设为 null,都不等于对象立刻不可达——栈帧可能还在、JIT 可能做了优化、或者存在隐式强引用(比如内部类持外层实例)。
典型场景:缓存、监听器注册、ThreadLocal、匿名内部类,这些地方最容易让对象“该死不死”。比如一个 Runnable 匿名类捕获了外部 Activity 实例,在 Android 中就会导致内存泄漏。
- 用
WeakReference或SoftReference管理非必需的长生命周期引用 - 显式清理:注销监听器、清空
ThreadLocal、关闭流或连接后置null - 别依赖
finalize():它不保证执行、不保证时机,且已被标记为 deprecated
GC 真正动手前的“缓冲期”:从不可达对象到内存释放之间有延迟
即使对象被判定为不可达,也不会在下一个 GC 周期就立刻释放。它先进入“待回收队列”,经历至少一次 Minor GC(年轻代)和可能的晋升,再经老年代 GC(如 G1 的 Mixed GC)才真正清理。这个过程可能横跨毫秒到数秒,取决于堆大小、GC 策略和当前负载。
影响明显的地方:频繁创建短命大对象(比如临时 byte[]),容易快速填满年轻代 Eden 区,引发频繁 Minor GC;而长期持有的小对象(比如配置类实例)如果意外滞留老年代,会拖慢 Full GC 频率与耗时。
- 避免在循环里
new大对象,优先复用或使用池化(如ByteBuffer.allocateDirect()要格外小心) - 观察 GC 日志里的
promotion failed或concurrent mode failure,这是对象过早晋升老年代的信号 -
-XX:+PrintGCDetails和jstat -gc <pid></pid>是定位生存周期异常的最直接手段
对象真的“死了”吗:Finalizer 和 Cleaner 的干扰逻辑
如果类重写了 finalize() 方法,或者用了 Cleaner(Java 9+ 推荐替代方案),对象在首次 GC 发现不可达后,并不会直接回收,而是被放入一个特殊队列,由低优先级的 Finalizer 线程异步执行清理逻辑。这会导致对象“复活”一次(比如在 finalize() 里重新赋值给某个静态变量),也极大延长其实际存活时间。
问题在于:Finalizer 线程可能严重滞后,甚至阻塞(比如 finalize() 里发生 I/O 或锁等待),进而拖慢整个 GC 流程。JDK 18 已默认禁用 finalization,但遗留代码仍可能触发。
- 绝不要在
finalize()中做关键资源释放,改用try-with-resources或显式close() - 用
Cleaner时,确保注册的清理动作是纯函数式、无阻塞、无副作用 - 通过
java.lang.ref.Cleaner的register()返回值及时取消注册,避免“幽灵引用”堆积
对象生存周期不是一条从 new 到 finalize 的直线,而是一张受引用类型、GC 策略、JIT 优化和代码写法共同拉扯的网。最容易被忽略的,其实是构造函数里的“this 泄露”和 Finalizer 线程的不可控延迟——它们不报错,但会让问题在高并发或长时间运行后才突然爆发。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











