3.1、JVM 学习——Object.finalize()方法

文章目录

      • 前言
      • Obejct.finalize()方法
        • 覆盖从写 finalize() 与 JVM 垃圾回收
          • 命令行 jstack 查看 JVM 后台驻留线程 Finalizer
        • 测试方法
        • finalize() 引发内存溢出
        • 最佳实践
      • 参考资料

前言

世界观第一,体能第二,技术第三。

Obejct.finalize()方法

JDK 中 Object 类有一个空实现的方法 finalize()。但是你无法直接通过Object对象调用这个方法,只能使用子类调用。
覆盖重写该方法的目的是完成一些收尾的工作。

protected void finalize() throws Throwable { }

覆盖从写 finalize() 与 JVM 垃圾回收

HotSpot 虚拟机使用可达性分析法判断一个对象是否还被引用, 如果 GC Root 的引用链中没有对该对象的引用了,那么对象也不会立即被回收,而是需要判断该对象所属类是否覆盖重写了 finalize()。
如果没有覆盖重写 finalize()方法,那么该对象就可以被进行垃圾回收了。
如果覆盖重写了finalize()方法,那么就要判断该方法是否被调用过了,如果该方法已经被调用了,也会对该对象进行垃圾回收。如果该对象的 finalize()方法 还没有被调用,那么会将该对象放入 java.lang.ref.Finalizer.ReferenceQueue队列中,然后由JVM的 后端驻留线程Finalizer依次调用队列中对象的finalize()方法。
如果覆盖重写 finalize()方法且进行了调用,调用过程没有导致该对象从新被 GC Root加入调用链,那么在下一次垃圾回收的时候该对象就会被回收。但是即使该对象被重新加入到 GC Root 调用链, finalize()也只会执行一次。

命令行 jstack 查看 JVM 后台驻留线程 Finalizer

使用 HotSpot 提供的命令行工具 jstack 我们可以查看到 JVM 中的后台驻留线程 Finalizer的相关信息。
该线程用于调用 java.lang.ref.ReferenceQueue 队列中对象的 finalize()方法
其优先级很低,所以并无法保证其运行的时效性。

“Finalizer” #3 daemon prio=8 os_prio=31 tid=0x00007fa36b855000 nid=0x5203 in Object.wait() [0x000070000c1bb000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000740f1fcf8> (a java.lang.ref.ReferenceQueueLock)atjava.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)−locked<0x0000000740f1fcf8>(ajava.lang.ref.ReferenceQueueLock)atjava.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)−locked<0x0000000740f1fcf8>(ajava.lang.ref.ReferenceQueueLock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

测试方法

子类覆盖重写 finalize()方法
手动调用 finalize()方法
手动触发垃圾回收 System.gc();
重复调用垃圾回收,根据打印日志判断 finalize()是否被调用一次,和对象是否被回收。

finalize() 引发内存溢出

这里推荐一篇很好的文章:http://it.deepinmind.com/gc/2014/05/13/debugging-to-understand-finalizer.html
大概的意思是,如果你覆盖重写了 finalize()方法,当系统在进行垃圾回收时会将该对象放入 java.lang.ref.ReferenceQueue 队列,等待后台驻留线程 Finalizer调用finalize()方法,又由于该线程优先级别很低,最后导致 java.lang.ref.ReferenceQueue 队列存储的内容越来越大,某种情况下导致内存溢出——因为垃圾回收并没有彻底完成。

回顾一下,Finalizable对象的生命周期和普通对象的行为是完全不同的,列举如下:

·JVM创建Finalizable对象
·JVM创建 java.lang.ref.Finalizer实例,指向刚创建的对象。
·java.lang.ref.Finalizer类持有新创建的java.lang.ref.Finalizer的实例。这使得下一次新生代GC无法回收这些对象。
·新生代GC无法清空Eden区,因此会将这些对象移到Survivor区或者老生代。
·垃圾回收器发现这些对象实现了finalize()方法。因为会把它们添加到java.lang.ref.Finalizer.ReferenceQueue队列中。
·Finalizer线程会处理这个队列,将里面的对象逐个弹出,并调用它们的finalize()方法。
·finalize()方法调用完后,Finalizer线程会将引用从Finalizer类中去掉,因此在下一轮GC中,这些对象就可以被回收了。
·Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。
·程序消耗了所有的可用资源,最后抛出OutOfMemoryError异常。

最佳实践

重写 finalize()方法 可能导致对象复活,但是至少会导致对象无法被立即回收。
由于垃圾回收的时机是我们无法掌控的,这就导致了内存溢出的风险,所以不建议自己覆盖重写这个方法,如果有什么需要特殊操作的,比如资源回收之类的,可以使用try···finally。
此外,jdk1.7支持 try(资源声明),不用明写 finally,后面自动回收资源了。

参考资料

[1]、《深入理解 Java 虚拟机》
[2]、http://it.deepinmind.com/gc/2014/05/13/debugging-to-understand-finalizer.html

你可能感兴趣的:(JVM)