zoukankan      html  css  js  c++  java
  • 引用计数和回收池 & java=null 的必要性

    java 使用的是垃圾回收和可达性分析

    oc 和 cocos2d-x 使用引用计数与回收池

    netty的bytebuf由于使用的直接内存,也使用引用计数

    交谈中提到了显式置为null,http://chenjingbo.iteye.com/blog/1980908  这篇文章有非常好的论述实践:

    且文中详细论述了:https://www.cnblogs.com/silyvin/p/9106570.html  中的显示大对象gc现象

    前言    

      之前看书的时候,看到了方法执行的内容,忽然就想到了这么一个有趣的东西.然后就特意开一个贴,把一些前人,大大的知识做一个汇总,做一下记录吧.

    正文

         相信,网上很多java性能优化的帖子里都会有这么一条 

    写道
    尽量把不使用的对象显式得置为null.这样有助于内存回收

         可以明确的说,这个观点是基本错误的.sun jdk远比我们想象中的机智.完全能判断出对象是否已经no ref..但是,我上面用的词是"基本".也就是说,有例外的情况.这里先把这个例外情况给提出来,后续我会一点点解释.这个例外的情况是, 方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译..总结这句话,就是

    写道
    除非在一个方法中,定义了一个非常大的对象,并且在后面又跟着一段非常耗时的操作.并且,该方法没有满足JIT编译条件,否则显式得设置 obj = null是完全没有必要的

     上面这句话有点绕,但是,上面说的每一个条件都是有意义的.这些条件分别是

    写道
    1 同一个方法中(不同方法已断链,有一个例外,就是后面hotspot没代码)
    2 定义了一个大对象(小对象没有意义)
    3 之后跟着一个非常耗时的操作. (提前断链释放空间)
    4 没有满足JIT编译条件(热点函数与循环,否则会把=null优化掉)

     上面4个条件缺一不可,把obj显式设置成null才是有意义的. 下面我会一一解释上面的这些条件

    在解释上面的条件之前,简略的说一下一些基础知识.

    (1)sun jdk的内存垃圾判定,是基于根搜索算法的.也就是说,在GC root为跟,能被搜索到的,就认为是存活对象,搜索不到的,则认为是"垃圾".

    (2)GC root  里和我们这篇文章有关的gc root是这一条

    写道
    Java Local
    Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

     这句话直接翻译就是说是"本地变量,例如方法的参数或者方法中创建的局部变量".如果换一种说法是,

    写道
    Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象。

    下面开始说四大条件. 我们测试是否被垃圾回收的方法是,申请一个64M的byte数组(作为大对象),然后调用System.gc();.运行的时候用 -verbose:gc 观察回收情况来判定是否会回收.

    同一个方法中

     这个条件是最容易理解的,如果大对象定义在其他方法中,那么是不需要设置成Null的,

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.   
    4.     public static void main(String[] args){  
    5.       
    6.         foo();  
    7.           
    8.         System.gc();  
    9.     }  
    10.       
    11.     public static void foo(){  
    12.         byte[] placeholder = new byte[64*1024*1024];  
    13.     }  
    14. }  

     对应的输出如下,可以看到64M的内存已经被回收

    写道
    D:>java -verbose:gc Test
    [GC 66798K->66120K(120960K), 0.0012225 secs]
    [Full GC 66120K->481K(120960K), 0.0059647 secs]

     其实很好理解,placeholder是foo方法的局部变量,在main方法中调用的时候,其实foo方法对应的栈帧已经结束.那么placeholder指向的大对象自然被gc的时候回收了.

    定义了一个大对象

    这句话的意思也很好理解.只有定义的是大的对象,我们才需要关心他尽快被回收.如果你只是定义了一个 String str = "abc"; 后续手动设置成null让gc回收是没有任何意义的.

    后面跟着一个非常耗时的操作

    这里理解是:后面的这个耗时的可能超过了一个GC的周期.例如

    Java代码  收藏代码
    1. public static void main(String[] args) throws Exception{  
    2.         byte[] placeholder = new byte[64*1024*1024];  
    3.         Thread.sleep(3000l);  
    4.         // dosomething  
    5.     }  

     在线程sleep的三秒内,可能jvm已经进行了好几次ygc.但是由于placeholder一直持有这个大对象,所以造成这个64M的大对象一直无法被回收,甚至有可能造成了满足进入old 区的条件.这个时候,在sleep之前,显式得把placeholder设置成Null是有意义的. 但是,

    写道
    如果没有这个耗时的操作,main方法可以非常快速的执行结束,方法返回,同时也会销毁对应的栈帧.那么就是回到第一个条件,方法已经执行结束,在下一次gc的时候,自然就会把对应的"垃圾"给回收掉.

    没有满足JIT编译条件

      jit编译的触发条件,这里就不多阐述了.对应的测试代码和前面一样

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         byte[] placeholder = new byte[64*1024*1024];  
    5.         placeholder = null;  
    6.         //do some  time-consuming operation  
    7.         System.gc();  
    8.     }  
    9. }  

     在解释执行中,我们认为

    写道
    placeholder = null;

     是有助于对这个大对象的回收的.在JIT编译下,我们可以通过强制执行编译执行,然后打印出对应的 ASM码的方式查看. 安装fast_debug版本的jdk请查看 

    使用-XX:+PrintAssembly打印asm代码遇到的问题

    命令是

    写道
    D:softwarejdk6_fastdebugjdk1.6.0_25fastdebugin>java -Xcomp -XX:+PrintAssembly Test > log.txt
    ASM 写道
    Decoding compiled method 0x0267f1c8:
    Code:
    [Disassembling for mach='i386']
    [Entry Point]
    [Verified Entry Point]
    [Constants]
    # {method} 'main' '([Ljava/lang/String;)V' in 'Test'
    # parm0: ecx = '[Ljava/lang/String;'
    # [sp+0x20] (sp of caller)
    ;; block B1 [0, 0]

    0x0267f2d0: mov %eax,-0x8000(%esp)
    0x0267f2d7: push %ebp
    0x0267f2d8: sub $0x18,%esp ;*ldc ; - Test::main@0 (line 7)
    ;; block B0 [0, 10]

    0x0267f2db: mov $0x4000000,%ebx
    0x0267f2e0: mov $0x20010850,%edx ; {oop({type array byte})}
    0x0267f2e5: mov %ebx,%edi
    0x0267f2e7: cmp $0xffffff,%ebx
    0x0267f2ed: ja 0x0267f37f
    0x0267f2f3: mov $0x13,%esi
    0x0267f2f8: lea (%esi,%ebx,1),%esi
    0x0267f2fb: and $0xfffffff8,%esi
    0x0267f2fe: mov %fs:0x0(,%eiz,1),%ecx
    0x0267f306: mov -0xc(%ecx),%ecx
    0x0267f309: mov 0x44(%ecx),%eax
    0x0267f30c: lea (%eax,%esi,1),%esi
    0x0267f30f: cmp 0x4c(%ecx),%esi
    0x0267f312: ja 0x0267f37f
    0x0267f318: mov %esi,0x44(%ecx)
    0x0267f31b: sub %eax,%esi
    0x0267f31d: movl $0x1,(%eax)
    0x0267f323: mov %edx,0x4(%eax)
    0x0267f326: mov %ebx,0x8(%eax)
    0x0267f329: sub $0xc,%esi
    0x0267f32c: je 0x0267f36f
    0x0267f332: test $0x3,%esi
    0x0267f338: je 0x0267f34f
    0x0267f33e: push $0x844ef48 ; {external_word}
    0x0267f343: call 0x0267f348
    0x0267f348: pusha 
    0x0267f349: call 0x0822c2e0 ; {runtime_call}
    0x0267f34e: hlt 
    0x0267f34f: xor %ebx,%ebx
    0x0267f351: shr $0x3,%esi
    0x0267f354: jae 0x0267f364
    0x0267f35a: mov %ebx,0xc(%eax,%esi,8)
    0x0267f35e: je 0x0267f36f
    0x0267f364: mov %ebx,0x8(%eax,%esi,8)
    0x0267f368: mov %ebx,0x4(%eax,%esi,8)
    0x0267f36c: dec %esi
    0x0267f36d: jne 0x0267f364 ;*newarray
    ; - Test::main@2 (line 7)
    0x0267f36f: call 0x025bb450 ; OopMap{off=164}
    ;*invokestatic gc
    ; - Test::main@7 (line 10)
    ; {static_call}
    0x0267f374: add $0x18,%esp
    0x0267f377: pop %ebp
    0x0267f378: test %eax,0x370100 ; {poll_return}
    0x0267f37e: ret 
    ;; NewTypeArrayStub slow case
    0x0267f37f: call 0x025f91d0 ; OopMap{off=180}
    ;*newarray
    ; - Test::main@2 (line 7)
    ; {runtime_call}
    0x0267f384: jmp 0x0267f36f
    0x0267f386: nop 
    0x0267f387: nop 
    ;; Unwind handler
    0x0267f388: mov %fs:0x0(,%eiz,1),%esi
    0x0267f390: mov -0xc(%esi),%esi
    0x0267f393: mov 0x198(%esi),%eax
    0x0267f399: movl $0x0,0x198(%esi)
    0x0267f3a3: movl $0x0,0x19c(%esi)
    0x0267f3ad: add $0x18,%esp
    0x0267f3b0: pop %ebp
    0x0267f3b1: jmp 0x025f7be0 ; {runtime_call}
    0x0267f3b6: hlt 
    0x0267f3b7: hlt 
    0x0267f3b8: hlt 
    0x0267f3b9: hlt 
    0x0267f3ba: hlt 
    0x0267f3bb: hlt 
    0x0267f3bc: hlt 
    0x0267f3bd: hlt 
    0x0267f3be: hlt 
    0x0267f3bf: hlt 
    [Stub Code]
    0x0267f3c0: nop ; {no_reloc}
    0x0267f3c1: nop 
    0x0267f3c2: mov $0x0,%ebx ; {static_stub}
    0x0267f3c7: jmp 0x0267f3c7 ; {runtime_call}
    [Exception Handler]
    0x0267f3cc: mov $0xdead,%ebx
    0x0267f3d1: mov $0xdead,%ecx
    0x0267f3d6: mov $0xdead,%esi
    0x0267f3db: mov $0xdead,%edi
    0x0267f3e0: call 0x025f9c40 ; {runtime_call}
    0x0267f3e5: push $0x83c8bc0 ; {external_word}
    0x0267f3ea: call 0x0267f3ef
    0x0267f3ef: pusha 
    0x0267f3f0: call 0x0822c2e0 ; {runtime_call}
    0x0267f3f5: hlt 
    [Deopt Handler Code]
    0x0267f3f6: push $0x267f3f6 ; {section_word}
    0x0267f3fb: jmp 0x025bbac0 ; {runtime_call}

     可以看到, placeholder = null; 这个语句被消除了! 也就是说,对于JIT编译以后的来说,压根不需要这个语句! 

    所以说,如果是解释执行的情况下,显式设置成Null是没有任何必要的!

    到这里,基本已经把文章开头说的那个论断给说明清楚了.但是,在文章的结尾,补充一下局部变量表会对内存回收有什么影响.这个例子参照<深入理解Java虚拟机:JVM高级特性与最佳实践> 一书

    我们认为

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         byte[] placeholder = new byte[64*1024*1024];  
    5.         //do some  time-consuming operation  
    6.         System.gc();  
    7.     }  
    8. }  

     这样的情况下,placeholder的对象是不会被回收的.可以理解..然后我们继续修改方法体

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         {  
    5.             byte[] placeholder = new byte[64*1024*1024];  
    6.         }  
    7.         System.gc();  
    8.     }  
    9. }  

     我们运行发现

    写道
    d:>java -verbose:gc Test
    [GC 66798K->66072K(120960K), 0.0021019 secs]
    [Full GC 66072K->66017K(120960K), 0.0069085 secs]

     垃圾收集器并不会把对象给回收..明明已经出了作用域,竟然还是不回收!. 好吧,继续修改例子

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         {  
    5.             byte[] placeholder = new byte[64*1024*1024];  
    6.         }  
    7.         int a = 0;  
    8.         System.gc();  
    9.     }  
    10. }  

     唯一的变化就是新增了一个 int a = 0; 继续看效果

    写道
    d:>java -verbose:gc Test
    [GC 66798K->66144K(120960K), 0.0011617 secs]
    [Full GC 66144K->481K(120960K), 0.0060882 secs]

     可以看到,大对象被回收了..这是一个神奇的例子..能想到这个,我对书的作者万分佩服! 但是这个例子的解释,在书中的解释有点泛(至少我刚开始没看懂),所以这里就仔细说明一下.

    要解释这个,先大概看一下  Java执行机制  里面局部变量表的部分.

    写道
    局部变量区用于存放方法中的局部变量和方法参数,.局部变量表用Slot为单位.jvm在实现的时候为了节省栈帧空间,做了一个简单的优化,就是slot的复用.如果当前字节码的PC计数器已经超出某些变量的作用域,那么这些变量的slot就可以给其他的复用.

    上面的这段话有点抽象,后面一个个解释.其实方法的局部变量表大小在javac的时候就已经确定了.

    写道
    在局部变量表的slot持有的某个对象,他是无法被垃圾回收的.因为局部变量表本来就是GC Root之一

    在class文件中,方法体对应的Code属性中就有对应的Locals属性,就是来记录局部变量表的大小的.例子如下:

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public void foo(int a,int b){  
    4.         int c = 0;  
    5.         return;  
    6.     }  
    7. }  

     通过 javac -g:vars Test 编译,然后,通过javap -verbose 查看

    写道
    public void foo(int, int);
    Code:
    Stack=1, Locals=4, Args_size=3
    0: iconst_0
    1: istore_3
    2: return
    LocalVariableTable:
    Start Length Slot Name Signature
    0 3 0 this LTest;
    0 3 1 a I
    0 3 2 b I
    2 1 3 c I

    可以看到,局部变量表的Slot数量是4个.分别是 this,a,b,c ..这个非常好理解.那么,什么叫做Slot的复用呢,继续看例子

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public void foo(int a,int b){  
    4.         {  
    5.             int d = 0;  
    6.         }  
    7.         int c = 0;  
    8.         return;  
    9.     }  
    10. }  

     在 int c = 0;之前新增一个作用域,里面定义了一个局部变量.如果没有slot复用机制,那么,理论上说,这个方法中局部变量表的slot个数应该是5个,但是,看具体的javap 输出

    写道
    public void foo(int, int);
    Code:
    Stack=1, Locals=4, Args_size=3
    0: iconst_0
    1: istore_3
    2: iconst_0
    3: istore_3
    4: return
    LocalVariableTable:
    Start Length Slot Name Signature
    2 0 3 d I
    0 5 0 this LTest;
    0 5 1 a I
    0 5 2 b I
    4 1 3 c I

    可以看到,对应的locals=4 ,也就是对应的slot个数还是4个. 通过查看对应的LocalVariableTable属性,可以看到,局部变量d和c都是在Slot[3]中. 这就是上面说的,在某个作用域结束以后,里面的对应的slot并没有马上消除,而是继续留着给下面的局部变量使用..按照这样理解,

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         {  
    5.             byte[] placeholder = new byte[64*1024*1024];  
    6.         }  
    7.         System.gc();  
    8.     }  
    9. }  

     这个例子中,在执行System.gc()的时候,虽然placeholder 的作用域已经结束,但是placeholder 对应的slot还存在,继续持有64M数组这个大对象,那么自然的,在GC的时候不会把对应的大对象给清理掉.而在

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         {  
    5.             byte[] placeholder = new byte[64*1024*1024];  
    6.         }  
    7.         int a = 0;  
    8.         System.gc();  
    9.     }  
    10. }  

     这个例子中,在System.gc的时候,placeholder对应的slot已经被a给占用了,那么对应的大对象就变成了无根的"垃圾",当然会被清楚.这一点,可以通过javap明显的看到

    写道
    public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
    Stack=1, Locals=2, Args_size=1
    0: ldc #2; //int 67108864
    2: newarray byte
    4: astore_1
    5: iconst_0
    6: istore_1
    7: invokestatic #3; //Method java/lang/System.gc:()V
    10: return
    LocalVariableTable:
    Start Length Slot Name Signature
    5 0 1 placeholder [B
    0 11 0 args [Ljava/lang/String;
    7 4 1 a I

    Exceptions:
    throws java.lang.Exception
    }

     可以看到,placeholder 和 a 都对应于Slot[1].

    这个例子说明的差不多了,在上面的基础上,再多一个例子

    Java代码  收藏代码
    1. public class Test  
    2. {  
    3.     public static void main(String[] args) throws Exception{  
    4.         {  
    5.             int b = 0;  
    6.             byte[] placeholder = new byte[64*1024*1024];  
    7.         }  
    8.         int a = 0;  
    9.         System.gc();  
    10.     }  
    11. }  

     这个代码中,这个64M的大对象会被GC回收吗..

    /**
     * https://www.cnblogs.com/silyvin/p/9106570.html
     * https://www.cnblogs.com/silyvin/p/9836797.html
     */
    public class Main {
    
        // -XX:+PrintGCDetails 
        public static void main(String[] args){
            {
                // 加上这句之后,int a=0不会导致holder回收
                int b = 0;
    
                // holder能否被回收的根本原因是局部变量表中的Slot是否还存有关于holder数组对象的引用
                byte[] holder = new byte[32*1024*1024];
    
                // 强制报废slot,导致内存回收
    //            holder = null;
            }
    
            // 此句在holder作用域之外执行一次读写操作,占用hodler slot,也可导致内存回收
            int a = 0;
            System.gc();
        }
    }
    
  • 相关阅读:
    使用Myeclipse + SVN + TaoCode 免费实现项目版本控制的详细教程
    国内的代码托管服务
    国内可用的SVN和Git代码托管网站汇总
    需求调研与分析流程
    如何做好新项目的需求调研?(一)
    如何进行有效的需求调研
    weblogic和tomcat
    同步变量也是变量
    并发编程的三个管理
    机器学习
  • 原文地址:https://www.cnblogs.com/silyvin/p/9836797.html
Copyright © 2011-2022 走看看