1. 程式人生 > >java中 obj=null對垃圾回收有用嗎

java中 obj=null對垃圾回收有用嗎

前言    

  之前看書的時候,看到了方法執行的內容,忽然就想到了這麼一個有趣的東西.然後就特意開一個貼,把一些前人,大大的知識做一個彙總,做一下記錄吧.

正文

     相信,網上很多java效能優化的帖子裡都會有這麼一條 

寫道 儘量把不使用的物件顯式得置為null.這樣有助於記憶體回收

     可以明確的說,這個觀點是基本錯誤的.sun jdk遠比我們想象中的機智.完全能判斷出物件是否已經no ref..但是,我上面用的詞是"基本".也就是說,有例外的情況.這裡先把這個例外情況給提出來,後續我會一點點解釋.這個例外的情況是, 方法前面中有定義大的物件,然後又跟著非常耗時的操作,且沒有觸發JIT編譯..總結這句話,就是

寫道 除非在一個方法中,定義了一個非常大的物件,並且在後面又跟著一段非常耗時的操作.並且,該方法沒有滿足JIT編譯條件,否則顯式得設定 obj = null是完全沒有必要的

 上面這句話有點繞,但是,上面說的每一個條件都是有意義的.這些條件分別是

寫道 1 同一個方法中
2 定義了一個大物件(小物件沒有意義)
3 之後跟著一個非常耗時的操作.
4 沒有滿足JIT編譯條件

 上面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.     public static void main(String[] args){  
  4.         foo();  
  5.         System.gc();  
  6.     }  
  7.     public static void foo(){  
  8.         byte[] placeholder = new byte[64*1024*1024];  
  9.     }  
  10. }  

 對應的輸出如下,可以看到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請檢視 

命令是

寫道 D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>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::[email protected] (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::[email protected] (line 7)
0x0267f36f: call 0x025bb450 ; OopMap{off=164}
;*invokestatic gc
; - Test::[email protected] (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::[email protected] (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回收嗎..

參考文章:

<深入理解Java虛擬機器:JVM高階特性與最佳實踐> 周志明的書