`
wenzongliang
  • 浏览: 447979 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

Effective Java Item7:Avoid Finalizers,解释为什么finalize是不安全的,不建议使用

 
阅读更多

我的上一篇博客:System.gc()和-XX:+DisableExplicitGC启动参数,以及DirectByteBuffer的内存释放  在讨论如何回收堆外内存的时候,提到“NIO中direct memory的释放并不是通过finalize(),因为finalize不安全而且影响能”。Effective Java一书中也提到:Avoid Finalizers。人都有潜在的叛逆意识,别人给的结论或者制定的规范,除非有足够的理由说服你,除非懂得这么做背后的原因,否则只能是死记硬背,没有形象深入的理解,不能学到真正的东西。本文通过自己的理解和一些实际的例子,和大家一起更形象的理解finalize。还是那句经典的话“talking is cheap,show me the code”。

 

我们先看下TestObjectHasFinalize这个类提供了finalize方法

 

[java] view plaincopy
  1. package finalize;  
  2.   
  3. public class TestObjectHasFinalize  
  4. {  
  5.     public static void main(String[] args)  
  6.     {  
  7.         while (true)  
  8.         {  
  9.             TestObjectHasFinalize heap = new TestObjectHasFinalize();  
  10.             System.out.println("memory address=" + heap);  
  11.         }  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void finalize() throws Throwable  
  16.     {  
  17.         super.finalize();  
  18.         System.out.println("finalize.");  
  19.     }  
  20. }  

运行这段程序,使用jmap命令查看对象占用的内存情况。

 

 

[plain] view plaincopy
  1. C:\Documents and Settings\Administrator>jps  
  2. 4232 Jps  
  3. 3236 TestObjectHasFinalize  
  4. 5272  
  5.   
  6. C:\Documents and Settings\Administrator>jmap -histo:live 3236  
  7.   
  8.  num     #instances         #bytes  class name  
  9. ----------------------------------------------  
  10.    1:        106983        3423456  java.lang.ref.Finalizer  
  11.    2:        106977         855816  finalize.TestObjectHasFinalize  
  12.    3:           642         841384  [I  
  13.    4:          5204         521984  <constMethodKlass>  
  14.    5:          8678         460712  <symbolKlass>  
  15.    6:          5204         460672  <methodKlass>  
  16.    7:          1694         206832  [C  
  17.    8:           351         206024  <constantPoolKlass>  
  18.    9:           351         142864  <instanceKlassKlass>  
  19.   10:           325         140040  <constantPoolCacheKlass>  
  20.   11:           421          79648  [B  
  21.   12:          1701          40824  java.lang.String  
  22.   13:           420          40320  java.lang.Class  
  23.   14:           519          33720  [S  
  24.   15:           547          32800  [[I  
  25.   16:           758          24256  java.util.TreeMap$Entry  
  26.   17:            94          18024  <methodDataKlass>  
  27.   18:            40          13120  <objArrayKlassKlass>  
  28.   19:           312          12776  [Ljava.lang.Object;  
  29.   20:            76           6080  java.lang.reflect.Method  
  30.   21:           181           5840  [Ljava.lang.String;  
  31.   22:            37           2664  java.lang.reflect.Field  
  32.   23:             8           2624  <typeArrayKlassKlass>  

可以发现占用内存较多的是java.lang.ref.Finalizer对象和TestObjectHasFinalize,为什么会有这么多个Finalizer对象呢?为什么要这么多的TestObjectHasFinalize对象呢?类似的,我们看下没有finalize()方法的情况

 

 

[java] view plaincopy
  1. public class TestObjectNoFinalize  
  2. {  
  3.     public static void main(String[] args)  
  4.     {  
  5.         while (true)  
  6.         {  
  7.             TestObjectNoFinalize heap = new TestObjectNoFinalize();  
  8.             System.out.println("object no finalize method." + heap);  
  9.         }  
  10.     }  
  11. }  
[plain] view plaincopy
  1. C:\Documents and Settings\Administrator>jps  
  2. 4436 TestObjectNoFinalize  
  3. 6012 Jps  
  4. 5272  
  5.   
  6. C:\Documents and Settings\Administrator>jmap -histo:live 4436  
  7.   
  8.  num     #instances         #bytes  class name  
  9. ----------------------------------------------  
  10.    1:          5203         521896  <constMethodKlass>  
  11.    2:          8677         460696  <symbolKlass>  
  12.    3:          5203         460584  <methodKlass>  
  13.    4:          1689         206544  [C  
  14.    5:           351         205992  <constantPoolKlass>  
  15.    6:           351         142864  <instanceKlassKlass>  
  16.    7:           325         140024  <constantPoolCacheKlass>  
  17.    8:           421          79640  [B  
  18.    9:          1696          40704  java.lang.String  
  19.   10:           420          40320  java.lang.Class  
  20.   11:           519          33720  [S  
  21.   12:           547          32800  [[I  
  22.   13:           758          24256  java.util.TreeMap$Entry  
  23.   14:           407          22088  [I  
  24.   15:            74          14720  <methodDataKlass>  
  25.   16:            40          13120  <objArrayKlassKlass>  
  26.   17:           312          12776  [Ljava.lang.Object;  
  27.   18:            76           6080  java.lang.reflect.Method  
  28.   19:           181           5840  [Ljava.lang.String;  
  29.   20:            37           2664  java.lang.reflect.Field  
  30.   21:             8           2624  <typeArrayKlassKlass>  
  31.   22:           100           1976  [Ljava.lang.Class;  
  32.   23:            20           1664  [Ljava.util.HashMap$Entry;  
  33.   24:            12           1440  <klassKlass>  
  34.   25:            59           1416  java.util.Hashtable$Entry  
  35.   26:            13            728  java.net.URL  
  36.   27:            18            720  java.util.HashMap  
  37.   28:             7            680  [Ljava.util.Hashtable$Entry;  
  38.   29:             6            672  java.lang.Thread  
  39.   30:            10            640  java.lang.reflect.Constructor  

可以发现,java.lang.ref.Finalizer和TestObjectHasFinalize没有占用大量的堆内存。没有提供finalize()方法的类,占用的堆内存更少,垃圾回收速度更快,而且JVM也不会创建那么多java.lang.ref.Finalizer对象

1、使用finalize会导致严重的内存消耗和性能损失

 

《Effective Java》中提到“使用finalizer会导致严重的性能损失。在我的机器上,创建和销毁一个简单对象的实践大约是5.6ns,增加finalizer后时间增加到2400ns。换言之,创建和销毁带有finalizer的对象会慢430倍”。额外的内存消耗这个很容看出,因为使用了finalize的时候,堆内存中会多出很多java.lang.ref.Finalizer对象。性能损失这个可以通过分析得出结论,因为使用了finalize的时候,堆内存会驻留大量的无用TestObjectHasFinalize对象。为什么有这么多TestObjectHasFinalize对象呢?很简单,垃圾回收的速度变慢了,对象的销毁速度小于对象的创建速度。为什么有这么多的java.lang.ref.Finalizer对象对象呢?这是JVM内部的机制,用来保证finalize只被调用一次。

 

2、JVM不确保finalize一定会被执行,而且执行finalize的时间也不确定。

从一个对象变为不可达,到其finalizer被执行,可能会经过任意长时间。这意味着你不能在finalizer中执行任何注重时间的任务。依靠finalizer来关闭文件就是一个严重错误,因为打开文件的描述符是一个有限资源。JVM会延迟执行finalizer,所以大量文件会被保持在打开状态,当一个程序不再能打开文件的时候,就会运行失败。

 

[java] view plaincopy
  1. import sun.misc.Unsafe;  
  2.   
  3. public class RevisedObjectInHeap  
  4. {  
  5.     private long address = 0;  
  6.   
  7.     private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();  
  8.   
  9.     public RevisedObjectInHeap()  
  10.     {  
  11.         address = unsafe.allocateMemory(2 * 1024 * 1024);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void finalize() throws Throwable  
  16.     {  
  17.         super.finalize();  
  18.         unsafe.freeMemory(address);  
  19.     }  
  20.   
  21.     public static void main(String[] args)  
  22.     {  
  23.         while (true)  
  24.         {  
  25.             RevisedObjectInHeap heap = new RevisedObjectInHeap();  
  26.             System.out.println("memory address=" + heap.address);  
  27.         }  
  28.     }  
  29.   
  30. }  

运行这段代码,很快就会出现堆外内存溢出。为什么呢?就是因为RevisedObjectInHeap.finalize方法不能及时执行,不能及时释放堆外内存。可以参考我的另一篇博客:java中使用堆外内存,关于内存回收需要注意的事和没有解决的遗留问题

 

当然使用finalize还有其他问题,具体的可以参考《Effective Java》。接下来介绍下JVM执行finalize方法的一些理论知识。实现了finalize()的对象,创建和回收的过程都更耗时。创建时,会新建一个额外的Finalizer 对象指向新创建的对象。 而回收时,至少需要经过两次GC

 

先来看下新建对象的时候发生的事,测试步骤如下:

1、在TestObjectNoFinalize和TestObjectHasFinalize这2个类的while循环中打上断点,在java.lang.ref.Finalizer的Finalizer()和register()打上断点

2、分别debug运行TestObjectNoFinalize和TestObjectHasFinalize,观察进入Finalizer断点的次数


测试结果如下:

1、TestObjectNoFinalize没有finalize()方法

      进入Finalizer断点的次数很少(3次,指向的是JarFile、Inflater、FileInputStream),之后进入while循环中的断点,不会再进入Finalizer中的断点。也就是说:创建TestObjectNoFinalize对象的时候,不会创建相应的Finalizer对象。

2、TestObjectHasFinalize提供了finalize()方法

      每次创建TestObjectHasFinalize对象的时候,都会创建相应的Finalizer对象指向它。


再看下回收对象的时候发生的事:加上-XX:+PrintGCDetails参数,观察下垃圾回收的过程。



可以看到TestObjectNoFinalize中都是在新生代中发生时的垃圾回收,很快就回收掉了内存。


TestObjectHasFinalize进行了几次新生代内存回收之后,频繁的进行[Full GC。这是以为回收内存的速度太慢,导致新生代内存不能及时释放,所以必须进行Full GC以期望获取空闲的内存空间。这个实验虽然不能直接证明至少需要进行2次GC,但是可以清楚的看到:含有finalize()的对象垃圾回收速度会很慢。

 

finalize机制的一些总结:

1、如果一个类A实现了finalize()方法,那么每次创建A类对象的时候,都会多创建一个Finalizer对象(指向刚刚新建的对象);如果类没有实现finalize()方法,那么不会创建额外的Finalizer对象

2、Finalizer内部维护了一个unfinalized链表,每次创建的Finalizer对象都会插入到该链表中。源码如下

 

[java] view plaincopy
  1. // 存储Finalizer对象的链表头指针  
  2. static private Finalizer unfinalized = null;  
  3. static private Object lock = new Object();  
  4.   
  5. private Finalizer next = null, prev = null;  
  6.   
  7. private void add()   
  8. {  
  9.     synchronized (lock)   
  10.     {  
  11.         if (unfinalized != null) {  
  12.         this.next = unfinalized;  
  13.         unfinalized.prev = this;  
  14.         }  
  15.         unfinalized = this;  
  16.     }  
  17. }  


3、如果类没有实现finalize方法,那么进行垃圾回收的时候,可以直接从堆内存中释放该对象。这是速度最快,效率最高的方式

 

4、如果类实现了finalize方法,进行GC的时候,如果发现某个对象只被java.lang.ref.Finalizer对象引用,那么会将该Finalizer对象加入到Finalizer类的引用队列(F-Queue)中,并从unfinalized链表中删除该结点。这个过程是JVM在GC的时候自动完成的。

 

[java] view plaincopy
  1.  /* Invoked by VM */      
  2. private void remove()  
  3. {  
  4.     synchronized (lock)   
  5.     {  
  6.         if (unfinalized == this) {  
  7.         if (this.next != null) {  
  8.             unfinalized = this.next;  
  9.         } else {  
  10.             unfinalized = this.prev;  
  11.         }  
  12.         }  
  13.         if (this.next != null) {  
  14.         this.next.prev = this.prev;  
  15.         }  
  16.         if (this.prev != null) {  
  17.         this.prev.next = this.next;  
  18.         }  
  19.         this.next = this;   /* Indicates that this has been finalized */  
  20.         this.prev = this;  
  21.     }  
  22. }  
[java] view plaincopy
  1. // 这就是F-Queue队列,存放的是Finalizer对象    
  2. static private ReferenceQueue queue = new ReferenceQueue();  

 

 

5、含有finalize()的对象从内存中释放,至少需要两次GC。

第一次GC, 检测到对象只有被Finalizer引用,将这个对象放入 java.lang.ref.Finalizer.ReferenceQueue 此时,因为Finalizer的引用,对象还无法被GC。java.lang.ref.Finalizer$FinalizerThread 会不停的清理Queue的对象,remove掉当前元素,并执行对象的finalize方法。清理后对象没有任何引用,在下一次GC被回收。

6、Finalizer是JVM内部的守护线程,优先级很低。Finalizer线程是个单一职责的线程。这个线程会不停的循环等待java.lang.ref.Finalizer.ReferenceQueue中的新增对象。一旦Finalizer线程发现队列中出现了新的对象,它会弹出该对象,调用它的finalize()方法,将该引用从Finalizer类中移除,因此下次GC再执行的时候,这个Finalizer实例以及它引用的那个对象就可以回垃圾回收掉了。

 

[java] view plaincopy
  1. private static class FinalizerThread extends Thread   
  2. {  
  3. FinalizerThread(ThreadGroup g) {  
  4.     super(g, "Finalizer");  
  5. }  
  6. public void run() {  
  7.     for (;;) {  
  8.     try {  
  9.         Finalizer f = (Finalizer)queue.remove();  
  10.         f.runFinalizer();  
  11.     } catch (InterruptedException x) {  
  12.         continue;  
  13.     }  
  14.     }  
  15. }  
  16. }  


7、使用finalize容易导致OOM,因为如果创建对象的速度很快,那么Finalizer线程的回收速度赶不上创建速度,就会导致内存垃圾越来越多

 

 

结束语:终于写完了,欢迎各位看官品读。有不到位或者不准确的地方, 欢迎提出意见。

分享到:
评论

相关推荐

    Effective Java 3rd edition(Effective Java第三版英文原版)附第二版

    Item 7: Eliminate obsolete object references Item 8: Avoid finalizers and cleaners Item 9: Prefer try-with-resources to try-finally 3 Methods Common to All Objects Item 10: Obey the general contract ...

    JAVA白皮书(英文版)

    1. Introduction to Java 1.1Beginnings of the Java Language Project 1.2Design Goals of Java 1.2.1Simple, Object Oriented, and Familiar 1.2.2Robust and Secure 1.2.3Architecture Neutral and ...

    gradle-7.5.1-all.zip 快速下载

    此版本包括使用 Java 18 构建代码和运行 Gradle、使用 Groovy 4 构建代码、响应速度更快的持续构建、改进的依赖解析诊断以及配置缓存改进以提高性能、为 JVM 提供 Adoptium 工具链等等,官方建议用户进行升级。...

    finalizers:愚蠢的终结者

    愚蠢的终结者建造go build 跑# List all objects blocked by a finalizer./finalizers# List all objects with finalizers./finalizers --all例子./finalizersNAMESPACE NAME APIVERSION KIND FINALIZERSp-nf5gh ...

    flexipatch-finalizer:自定义预处理器,用于从flexipatch版本中删除未选择的补丁,从而保留最终的补丁版本

    flexipatch-finalizer是一个自定义预处理器,它使用相同的配置文件,并剥离所有未使用代码的flexipatch构建,从而保留应用了选定补丁的软件构建。 该终结器的示例flexipatch构建可用于: :warning: 请务必注意,...

    愚蠢的终结者-Golang开发

    愚蠢的终结者Build go build Run#列出被终结者阻止的所有对象./finalizers#列出所有带有终结器的对象./finalizers --all Ex愚蠢的终结者Build go build Run#列出被终结器阻止的所有对象./finalizers#列出所有...

    python3.6.5参考手册 chm

    Python参考手册,官方正式版参考手册,chm版。以下摘取部分内容:Navigation index modules | next | Python » 3.6.5 Documentation » Python Documentation contents What’s New in Python ...

Global site tag (gtag.js) - Google Analytics