那些年被誤導的人們-java中將不再使用的區域性物件賦值為null,對垃圾回收有用嗎?
今天又在重構“祖傳程式碼”,看到了這一幕:
心好累,直接抄網文了。。。
誤人子弟之一,估計是 寫超長函式(不會合理拆分函式,流水賬程式碼)或者大物件不會使用WeakReferenc,或者動不動就習慣手動GC造成的:
Java : 物件不再使用時,為什麼要賦值為null?
前言
許多Java開發者都曾聽說過“不使用的物件應手動賦值為null“這句話,而且好多開發者一直信奉著這句話;問其原因,大都是回答“有利於GC更早回收記憶體,減少記憶體佔用”,但再往深入問就回答不出來了。
鑑於網上有太多關於此問題的誤導,本文將通過例項,深入JVM剖析“物件不再使用時賦值為null”這一操作存在的意義,供君參考。本文儘量不使用專業術語,但仍需要你對JVM有一些概念。
示例程式碼
我們來看看一段非常簡單的程式碼:
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}
我們在if中例項化了一個數組placeHolder,然後在if的作用域外通過System.gc();手動觸發了GC,其用意是回收placeHolder,因為placeHolder已經無法訪問到了。來看看輸出:
65536
[GC 68239K->65952K(125952K), 0.0014820 secs]
[Full GC 65952K->65881K(125952K), 0.0093860 secs]
Full GC 65952K->65881K(125952K)代表的意思是:本次GC後,記憶體佔用從65952K降到了65881K。意思其實是說GC沒有將placeHolder回收掉,是不是不可思議?
下面來看看遵循“不使用的物件應手動賦值為null“的情況:
public static void main(String[] args) { if (true) { byte[] placeHolder = new byte[64 * 1024 * 1024]; System.out.println(placeHolder.length / 1024); placeHolder = null; } System.gc(); }
其輸出為:
65536
[GC 68239K->65952K(125952K), 0.0014910 secs]
[Full GC 65952K->345K(125952K), 0.0099610 secs]
這次GC後記憶體佔用下降到了345K,即placeHolder被成功回收了!對比兩段程式碼,僅僅將placeHolder賦值為null就解決了GC的問題,真應該感謝“不使用的物件應手動賦值為null“。
等等,為什麼例子裡placeHolder不賦值為null,GC就“發現不了”placeHolder該回收呢?這才是問題的關鍵所在。
執行時棧
典型的執行時棧
如果你瞭解過編譯原理,或者程式執行的底層機制,你會知道方法在執行的時候,方法裡的變數(區域性變數)都是分配在棧上的;當然,對於Java來說,new出來的物件是在堆中,但棧中也會有這個物件的指標,和int一樣。
比如對於下面這段程式碼:
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}
其執行時棧的狀態可以理解成:
索引變數1a2b3c
“索引”表示變數在棧中的序號,根據方法內程式碼執行的先後順序,變數被按順序放在棧中。
再比如:
public static void main(String[] args) {
if (true) {
int a = 1;
int b = 2;
int c = a + b;
}
int d = 4;
}
這時執行時棧就是:
索引變數1a2b3c4d
容易理解吧?其實仔細想想上面這個例子的執行時棧是有優化空間的。
Java的棧優化
上面的例子,main()方法執行時佔用了4個棧索引空間,但實際上不需要佔用這麼多。當if執行完後,變數a、b和c都不可能再訪問到了,所以它們佔用的1~3的棧索引是可以“回收”掉的,比如像這樣:
索引變數1a2b3c1d
變數d重用了變數a的棧索引,這樣就節約了記憶體空間。
提醒
上面的“執行時棧”和“索引”是為方便引入而故意發明的詞,實際上在JVM中,它們的名字分別叫做“區域性變量表”和“Slot”。而且區域性變量表在編譯時即已確定,不需要等到“執行時”。
GC一瞥
這裡來簡單講講主流GC裡非常簡單的一小塊:如何確定物件可以被回收。另一種表達是,如何確定物件是存活的。
仔細想想,Java的世界中,物件與物件之間是存在關聯的,我們可以從一個物件訪問到另一個物件。如圖所示。
再仔細想想,這些物件與物件之間構成的引用關係,就像是一張大大的圖;更清楚一點,是眾多的樹。
如果我們找到了所有的樹根,那麼從樹根走下去就能找到所有存活的物件,那麼那些沒有找到的物件,就是已經死亡的了!這樣GC就可以把那些物件回收掉了。
現在的問題是,怎麼找到樹根呢?JVM早有規定,其中一個就是:棧中引用的物件。也就是說,只要堆中的這個物件,在棧中還存在引用,就會被認定是存活的。
提醒
上面介紹的確定物件可以被回收的演算法,其名字是“可達性分析演算法”。
JVM的“bug”
我們再來回頭看看最開始的例子:
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}
看看其執行時棧:
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 args [Ljava/lang/String;
5 12 1 placeHolder [B
棧中第一個索引是方法傳入引數args,其型別為String[];第二個索引是placeHolder,其型別為byte[]。
聯絡前面的內容,我們推斷placeHolder沒有被回收的原因:System.gc();觸發GC時,main()方法的執行時棧中,還存在有對args和placeHolder的引用,GC判斷這兩個物件都是存活的,不進行回收。也就是說,程式碼在離開if後,雖然已經離開了placeHolder的作用域,但在此之後,沒有任何對執行時棧的讀寫,placeHolder所在的索引還沒有被其他變數重用,所以GC判斷其為存活。
為了驗證這一推斷,我們在System.gc();之前再宣告一個變數,按照之前提到的“Java的棧優化”,這個變數會重用placeHolder的索引。
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
int replacer = 1;
System.gc();
}
看看其執行時棧:
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
5 12 1 placeHolder [B
19 4 1 replacer I
不出所料,replacer重用了placeHolder的索引。來看看GC情況:
65536
[GC 68239K->65984K(125952K), 0.0011620 secs]
[Full GC 65984K->345K(125952K), 0.0095220 secs]
placeHolder被成功回收了!我們的推斷也被驗證了。
再從執行時棧來看,加上int replacer = 1;和將placeHolder賦值為null起到了同樣的作用:斷開堆中placeHolder和棧的聯絡,讓GC判斷placeHolder已經死亡。
現在算是理清了“不使用的物件應手動賦值為null“的原理了,一切根源都是來自於JVM的一個“bug”:程式碼離開變數作用域時,並不會自動切斷其與堆的聯絡。為什麼這個“bug”一直存在?你不覺得出現這種情況的概率太小了麼?算是一個tradeoff了。
總結
希望看到這裡你已經明白了“不使用的物件應手動賦值為null“這句話背後的奧義。我比較贊同《深入理解Java虛擬機器》作者的觀點:在需要“不使用的物件應手動賦值為null“時大膽去用,但不應當對其有過多依賴,更不能當作是一個普遍規則來推廣。
參考
周志明. 深入理解Java虛擬機器:JVM高階特性與最佳實踐[M]. 機械工業出版社, 2013.
推薦閱讀(點選即可跳轉閱讀)
2.面試題內容聚合
3.設計模式內容聚合
5.多執行緒內容聚合
釋出於 2019-11-14 from:https://zhuanlan.zhihu.com/p/91763061java中將物件賦值為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 LocalLocal 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程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args){
- foo();
- System.gc();
- }
- publicstaticvoidfoo(){
- byte[]placeholder=newbyte[64*1024*1024];
- }
- }
對應的輸出如下,可以看到64M的記憶體已經被回收
寫道 D:\>java -verbose:gc Test[GC 66798K->66120K(120960K), 0.0012225 secs]
[Full GC66120K->481K(120960K), 0.0059647 secs]
其實很好理解,placeholder是foo方法的區域性變數,在main方法中呼叫的時候,其實foo方法對應的棧幀已經結束.那麼placeholder指向的大物件自然被gc的時候回收了.
定義了一個大物件
這句話的意思也很好理解.只有定義的是大的物件,我們才需要關心他儘快被回收.如果你只是定義了一個 String str = "abc"; 後續手動設定成null讓gc回收是沒有任何意義的.
後面跟著一個非常耗時的操作
這裡理解是:後面的這個耗時的可能超過了一個GC的週期.例如
Java程式碼- publicstaticvoidmain(String[]args)throwsException{
- byte[]placeholder=newbyte[64*1024*1024];
- Thread.sleep(3000l);
- //dosomething
- }
線上程sleep的三秒內,可能jvm已經進行了好幾次ygc.但是由於placeholder一直持有這個大物件,所以造成這個64M的大物件一直無法被回收,甚至有可能造成了滿足進入old 區的條件.這個時候,在sleep之前,顯式得把placeholder設定成Null是有意義的. 但是,
寫道 如果沒有這個耗時的操作,main方法可以非常快速的執行結束,方法返回,同時也會銷燬對應的棧幀.那麼就是回到第一個條件,方法已經執行結束,在下一次gc的時候,自然就會把對應的"垃圾"給回收掉.沒有滿足JIT編譯條件
jit編譯的觸發條件,這裡就不多闡述了.對應的測試程式碼和前面一樣
Java程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- byte[]placeholder=newbyte[64*1024*1024];
- placeholder=null;
- //dosometime-consumingoperation
- System.gc();
- }
- }
在解釋執行中,我們認為
寫道 placeholder = null;是有助於對這個大物件的回收的.在JIT編譯下,我們可以通過強制執行編譯執行,然後打印出對應的 ASM碼的方式檢視. 安裝fast_debug版本的jdk請檢視
使用-XX:+PrintAssembly列印asm程式碼遇到的問題
命令是
寫道 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::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程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- byte[]placeholder=newbyte[64*1024*1024];
- //dosometime-consumingoperation
- System.gc();
- }
- }
這樣的情況下,placeholder的物件是不會被回收的.可以理解..然後我們繼續修改方法體
Java程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- {
- byte[]placeholder=newbyte[64*1024*1024];
- }
- System.gc();
- }
- }
我們執行發現
寫道 d:\>java -verbose:gc Test[GC 66798K->66072K(120960K), 0.0021019 secs]
[Full GC66072K->66017K(120960K), 0.0069085 secs]
垃圾收集器並不會把物件給回收..明明已經出了作用域,竟然還是不回收!. 好吧,繼續修改例子
Java程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- {
- byte[]placeholder=newbyte[64*1024*1024];
- }
- inta=0;
- System.gc();
- }
- }
唯一的變化就是新增了一個 int a = 0; 繼續看效果
寫道 d:\>java -verbose:gc Test[GC 66798K->66144K(120960K), 0.0011617 secs]
[Full GC66144K->481K(120960K), 0.0060882 secs]
可以看到,大物件被回收了..這是一個神奇的例子..能想到這個,我對書的作者萬分佩服! 但是這個例子的解釋,在書中的解釋有點泛(至少我剛開始沒看懂),所以這裡就仔細說明一下.
要解釋這個,先大概看一下 Java執行機制 裡面區域性變量表的部分.
寫道 區域性變數區用於存放方法中的區域性變數和方法引數,.區域性變量表用Slot為單位.jvm在實現的時候為了節省棧幀空間,做了一個簡單的優化,就是slot的複用.如果當前位元組碼的PC計數器已經超出某些變數的作用域,那麼這些變數的slot就可以給其他的複用.上面的這段話有點抽象,後面一個個解釋.其實方法的區域性變量表大小在javac的時候就已經確定了.
寫道 在區域性變量表的slot持有的某個物件,他是無法被垃圾回收的.因為區域性變量表本來就是GC Root之一在class檔案中,方法體對應的Code屬性中就有對應的Locals屬性,就是來記錄區域性變量表的大小的.例子如下:
Java程式碼- publicclassTest
- {
- publicvoidfoo(inta,intb){
- intc=0;
- return;
- }
- }
通過 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程式碼- publicclassTest
- {
- publicvoidfoo(inta,intb){
- {
- intd=0;
- }
- intc=0;
- return;
- }
- }
在 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程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- {
- byte[]placeholder=newbyte[64*1024*1024];
- }
- System.gc();
- }
- }
這個例子中,在執行System.gc()的時候,雖然placeholder 的作用域已經結束,但是placeholder 對應的slot還存在,繼續持有64M陣列這個大物件,那麼自然的,在GC的時候不會把對應的大物件給清理掉.而在
Java程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- {
- byte[]placeholder=newbyte[64*1024*1024];
- }
- inta=0;
- System.gc();
- }
- }
這個例子中,在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程式碼- publicclassTest
- {
- publicstaticvoidmain(String[]args)throwsException{
- {
- intb=0;
- byte[]placeholder=newbyte[64*1024*1024];
- }
- inta=0;
- System.gc();
- }
- }
這個程式碼中,這個64M的大物件會被GC回收嗎..
參考文章:
http://icyfenix.iteye.com/blog/900737
http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html
Java執行機制
前言
這篇裡的東西,其實是我在草稿箱裡找到的.因為當時寫的比較粗,而且這個題目的內容沒有完結..所以一直沒有釋出.但是後續有篇文章
java中 obj=null對垃圾回收有用嗎
要引用裡面區域性變量表的知識,所以就先把這個半吊子釋出出來,後續慢慢補充.
類執行機制
jvm採用中間碼來實現執行.其中,方法執行的指令有下面幾個
(1)invokestatic 執行static方法
(2)invokevirtual 呼叫物件例項方法
(3)invokeinterface 呼叫介面方法
(4)invokespecial 呼叫private方法和Init方法
上面說的其實有點籠統,invokevirtual和invokespecial的知識可以看一下Java方法分派裡面說的比較詳細.
另外,jdk7以後新增了一條方法執行invokedynamic 提供了一條對動態語言的支援.可惜在jdk7沒有在java語言中支援該指令. 可以看一下http://rednaxelafx.iteye.com/blog/477934
sun JDK基於棧的體系來執行位元組碼.執行緒在建立後,都會產生程式計數器(PC register)和棧(Stack),其中程式計數器存放要執行的指令在方法內的偏移量,棧當中存放棧幀,每個方法每次呼叫都會產生棧幀.
注:該圖來自 畢玄的書 <釋出式Java應用基礎和實踐> 區域性變數區用於存放方法中的區域性變數和引數,運算元棧用於存放方法執行過程中的中間結果.
可以這麼理解,任何在方法體中特意定義過的區域性變數,那麼都會放到區域性變量表(方法引數也會放這裡).其他的中間結果.具體看下面的例子 Java程式碼
- publicclassTest
- {
- publicintadd(inta,intb){
- intc=a+1;
- returnc*b;
- }
- }
Code:
Stack=2, Locals=4, Args_size=3
0: iload_1 //將區域性變量表中第一個值壓入運算元棧(這個值對應add方法中的引數a)
1: iconst_1 //將int 型別的 1 放入運算元棧.
2: iadd //執行add指令. 將運算元棧頂兩個元素相加,然後把結果放到棧頂 (a + 1)
3: istore_3 //把棧頂元素放到區域性變量表中的第三個位置.也就是c
4: iload_3 //把區域性變量表第三個元素壓入棧頂,也就是c
5: iload_2 //把區域性變量表第二個元素壓入棧頂,也就是b
6: imul //執行mul指令,將棧頂兩個相乘,然後把結果放入棧頂,也就是 c * b
7: ireturn //把棧頂元素返回.
LineNumberTable:
line 8: 0
line 9: 4
}
補充說明一下,
寫道 其實區域性變量表是有第零個元素的.不過一般這個元素都是指向this.如果把方法改成static,就可以看到第一個方法引數(也就是a) 就是以iload_0來壓棧了.棧頂快取
從上面的例子看到,我們計算 c * b 的時候,需要從存放c的暫存器(也就是第三個區域性變量表的元素)先壓入運算元棧的棧頂,然後再計算相乘操作.而棧頂快取的效果就是,取消壓棧的這次操作,直接把暫存器的資料拿來做相乘操作,然後把結果壓入棧.
使用-XX:+PrintAssembly列印asm程式碼遇到的問題
要用PrintAssembly的目的 應該會另開帖子說明,本帖只是為了記錄為了簡單的記錄使用這個命令遇到的問題.
1 ,直接使用,用的是
寫道 C:\Users\zhenghui>java -versionjava version "1.7.0_25"
Java(TM) SE Runtime Environment (build 1.7.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 23.25-b01, mixed mode)
該版本,當然,必須用不了咯..繼續google.發現,需要用fastjson版本的jdk.然後,繼續找.
2,找到對應的下載地址.http://download.java.net/jdk6/6u25/promoted/b03/index.html
注意,需要下載debug版本的.下載下來是一個jar,雙擊執行.然後就可以安裝.
這裡下載的b03版本,有可能在window下跑不通,每次執行都會造成jvm crash.可以換一下b01的試試
http://download.java.net/jdk6/6u25/promoted/b01/index.html
3 然後,就執行試試.繼續遇到問題
寫道 D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test
VM option '+UnlockDiagnosticVMOptions'
VM option '+PrintAssembly'
Java HotSpot(TM) Client VM warning: PrintAssembly is enabled; turning on DebugNo
nSafepoints to gain additional output
Could not load hsdis-i386.dll; library not loadable; PrintAssembly is disabled
4 後續就是找hsdis-i386.dll 的問題了.這也是我用時間最久的地方.我按照stackoverflow的說明,自己build這貨,但是死的很慘,一直沒成功.具體就不多說了,最後還是萬能的撒迦告訴了我答案.
RednaxelaFX 寫道 是撒迦…這個疑問在之前某帖的回覆裡有提到:HotSpot的JIT編譯器遇到簡單無限迴圈時
RednaxelaFX 寫道 lvgang 寫道 好文,很有深度,就是看不太懂,呵呵。有個問題向博主請教,從上面打印出來的彙編程式碼來看,貌似是 gnu as,windows 上也能用 gnu as?
是的,這個是由GNU binutils裡的as提供反彙編功能。
Sun HotSpot需要一個反彙編外掛才可以使用-XX:+PrintAssembly引數來列印JIT編譯生成的程式碼。該外掛有一組通用介面,本來是可以用任意反彙編器套個介面卡就行。官方提供了一個現成的版本(hsdis)是基於gas的,我懶於是就直接用它了。在Windows上直接build我還沒成功過,用MinGW和Cygwin都試過不行。我用的版本是在Ubuntu上cross-compile出來的,根據外掛作者提供的cross-compile指引來做沒有遇到問題。
編譯出來的hsdis-i386.dll放到JDK安裝目錄中jre/bin/server和jre/bin/client中即可。
不太記得是不是從Sun JDK 6 update 20開始,在product build的HotSpot裡要用-XX:+PrintAssembly引數必須同時帶上-XX:+UnlockDiagnosticVMOptions引數才可以。
Command prompt程式碼
- java-XX:+UnlockDiagnosticVMOptions-XX:+PrintAssemblyYourMainClass
debug與fastdebug build就不用帶。
而在比較老的Sun JDK 6裡這個外掛的名字要改為hdis-i486.dll才行。具體是從哪個版本開始變的我可以回頭查檢視。
如果有人需要我編譯好的這個外掛的話,待會兒可以上傳一個到圈子共享裡。
已經上傳到圈子的共享裡了
OpenJDK 7裡可以看到還有另外一個附加的選項,-XX:PrintAssemblyOptions,可以用來向反編譯外掛傳遞引數。
藉助HotSpot SA來反彙編
這帖提到的也是其中一個辦法。看圖:
=======================================
上面是針對Sun JDK的HotSpot而言。
JRockit的話要用別的辦法,不過由於Oracle在輸出的日誌裡說那資訊是confidential的,所以抱歉我不能在這裡說。
IBM J9的話我還沒找到簡單的辦法。
Harmony、Jikes RVM、Maxine這些都有提供命令列引數可以讓JVM把動態編譯的彙編吐出來。
然後 在JE的虛擬機器圈子裡找到了下載連結.
http://hllvm.group.iteye.com/group/share
最後就是下載對應的hsdis-i386.dll ,在DK目錄下jre/bin/client和jre/bin/server中各放一份 .這個問題終於搞定..
在實際的使用中,我們通過直接加-XX:+PrintAssembly 列印ASM碼會有兩個問題
1 ASM碼非常多.因為系統會打印出類似loadclass toString對應這些方法的ASM碼.但是我們可能只關心對應的某一個方法而已.系統會列印8W行+的輸出,但是我們可能只關心裡面的100行
2 經常會幫我們內聯.這個其實很糾結.之前為了這個內聯,每次都想方設法如何讓方法不被內聯.當然,對應的解決方法也很簡單.
對應的程式碼(我懶得自己寫,就直接copy網上的了)
Java程式碼- publicclassTest{
- inta=1;
- staticintb=2;
- publicintsum(intc){
- returna+b+c;
- }
- publicstaticvoidmain(String[]args){
- newTest().sum(3);
- }
- }
執行的程式碼如下
寫道 D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -Xcomp -XX:+PrintAssembly -XX:CompileCommand=dontinline,*Test.sum -XX:CompileCommand=compileonly,*Test.sum Test > log.txt其中,
-XX:CompileCommand=dontinline,*Test.sum 這個表示不要把sum方法給內聯了.這是解決內聯問題
-XX:CompileCommand=compileonly,*Test.sum 這個表示只編譯sum方法,這樣的話,只會輸出sum方法的ASM碼.
對應的輸出如下
寫道 Java HotSpot(TM) Client VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional outputVM option '+PrintAssembly'
VM option 'CompileCommand=dontinline,*Test.sum'
VM option 'CompileCommand=compileonly,*Test.sum'
CompilerOracle: dontinline *Test.sum
CompilerOracle: compileonly *Test.sum
Loaded disassembler from D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\jre\bin\client\hsdis-i386.dll
Decoding compiled method 0x026ab608:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Constants]
# {method} 'sum' '(I)I' in 'Test' //這個好理解,記錄一下這個方法名對應的描述符
# this: ecx = 'Test' //表示this指標在ecx暫存器中
# parm0: edx = int //sum 方法對應的引數在edx暫存器中.
# [sp+0x20] (sp of caller)
;; block B1 [0, 0]
0x026ab700: nop
0x026ab701: nop
0x026ab702: nop
0x026ab703: nop
0x026ab704: nop
0x026ab705: nop
0x026ab706: nop
0x026ab707: cmp 0x4(%ecx),%eax
0x026ab70a: jne 0x0266ad90 ; {runtime_call}
[Verified Entry Point]
0x026ab710: mov %eax,-0x8000(%esp) //檢查棧溢位
0x026ab717: push %ebp //儲存上一棧幀基址
0x026ab718: sub $0x18,%esp ;*aload_0 //給新棧幀分配空間.這個18很奇怪,我試了好多,無聊方法寫成什麼樣,都是$0x18.
; - Test::sum@0 (line 13)
;; block B0 [0, 10]
0x026ab71b: mov 0x8(%ecx),%eax ;*getfield a //獲取例項變數a,放入eax暫存器中, %ecx在上面已經說了,是存放this指標的暫存器. 0x8(%ecx)表示越過test物件頭(物件頭佔8個位元組,後面就是跟著例項變數a的記憶體位置)
; - Test::sum@1 (line 13)
0x026ab71e: mov $0x2024d7d8,%esi ; {oop('Test')}//獲取Test在方法區的指標, 可以看標記oop(methodName),$0x2024d7d8就是對應的Test方法區位置
0x026ab723: mov 0x150(%esi),%esi ;*getstatic b //獲取對應的類變數b,放到esi暫存器中,0x150(%esi)表示在Test方法區指標開始的150偏移量的位置存放類變數b
; - Test::sum@4 (line 13)
0x026ab729: add %esi,%eax// esi存放的是b,eax存放的是a.兩者相加,放到eax暫存器中
0x026ab72b: add %edx,%eax //edx 存放的是sum方法對應的引數c,eax存放著a+b的和.兩者相加放到eax暫存器中
0x026ab72d: add $0x18,%esp //esp為對應的棧幀指標,之前sub 0x18 ,現在加回去.也就是撤銷棧幀
0x026ab730: pop %ebp //恢復上一個棧幀
0x026ab731: test %eax,0x230100 ; {poll_return} //輪詢方法返回處的SafePoint
0x026ab737: ret //返回.
0x026ab738: nop
0x026ab739: nop
;; Unwind handler
0x026ab73a: mov %fs:0x0(,%eiz,1),%esi
0x026ab742: mov -0xc(%esi),%esi
0x026ab745: mov 0x198(%esi),%eax
0x026ab74b: movl $0x0,0x198(%esi)
0x026ab755: movl $0x0,0x19c(%esi)
0x026ab75f: add $0x18,%esp
0x026ab762: pop %ebp
0x026ab763: jmp 0x026a7be0 ; {runtime_call}
0x026ab768: hlt
0x026ab769: hlt
0x026ab76a: hlt
0x026ab76b: hlt
0x026ab76c: hlt
0x026ab76d: hlt
0x026ab76e: hlt
0x026ab76f: hlt
[Exception Handler]
[Stub Code]
0x026ab770: mov $0xdead,%ebx ; {no_reloc}
0x026ab775: mov $0xdead,%ecx
0x026ab77a: mov $0xdead,%esi
0x026ab77f: mov $0xdead,%edi
0x026ab784: call 0x026a9c40 ; {runtime_call}
0x026ab789: push $0x83c8bc0 ; {external_word}
0x026ab78e: call 0x026ab793
0x026ab793: pusha
0x026ab794: call 0x0822c2e0 ; {runtime_call}
0x026ab799: hlt
[Deopt Handler Code]
0x026ab79a: push $0x26ab79a ; {section_word}
0x026ab79f: jmp 0x0266bac0 ; {runtime_call}
指令碼的解析,上面基本都寫了.
參照連結
http://stackoverflow.com/questions/1503479/how-to-see-jit-compiled-code-in-jvm/4149878#4149878
http://hllvm.group.iteye.com/group/topic/21769
https://blogs.oracle.com/kto/entry/mustang_jdk_6_0_fastdebug
HotSpot的JIT編譯器遇到簡單無限迴圈時
對下面這種帶有簡單無限迴圈的Java程式,
Java程式碼- //java-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop
- publicclassTestC2InfiniteLoop{
- publicstaticvoidfoo(){
- while(true){inti=1;}
- }
- publicstaticvoidmain(String[]args){
- foo();
- }
- }
HotSpot的JIT編譯器會:
1、client模式:執行命令:
- java-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop
C1會編譯程式碼,會將迴圈內無用的程式碼都消除掉,但不會把迴圈本身消除:
- 1%TestC2InfiniteLoop::foo@0(5bytes)
- Decodingcompiledmethod0x00bc6748:
- Code:
- [Disassemblingformach='i386']
- [EntryPoint]
- [VerifiedEntryPoint]
- ;;blockB2[0,0]
- 0x00bc6810:mov%eax,-0x4000(%esp)
- 0x00bc6817:push%ebp
- 0x00bc6818:mov%esp,%ebp
- 0x00bc681a:sub$0x18,%esp;*iconst_1
- ;-TestC2InfiniteLoop::foo@0(line5)
- ;;blockB3[0,0]
- 0x00bc681d:nop
- 0x00bc681e:nop
- 0x00bc681f:nop;OopMap{off=16}
- ;*goto
- ;-TestC2InfiniteLoop::foo@2(line5)
- ;;blockB0[0,2]
- 0x00bc6820:test%eax,0x970100;{poll}
- ;;26branch[AL][B0]
- 0x00bc6826:jmp0x00bc6820;*goto
- ;-TestC2InfiniteLoop::foo@2(line5)
- ;;blockB1[0,0]
- 0x00bc6828:mov%eax,-0x4000(%esp)
- 0x00bc682f:push%ebp
- 0x00bc6830:mov%esp,%ebp
- 0x00bc6832:sub$0x18,%esp
- 0x00bc6835:mov%ecx,(%esp)
- 0x00bc6838:call0x082ea120;{runtime_call}
- ;;20branch[AL][B0]
- 0x00bc683d:jmp0x00bc6820
- 0x00bc683f:nop
- 0x00bc6840:nop
- 0x00bc6841:hlt
- 0x00bc6842:hlt
- 0x00bc6843:hlt
- 0x00bc6844:hlt
- 0x00bc6845:hlt
- 0x00bc6846:hlt
- 0x00bc6847:hlt
- 0x00bc6848:hlt
- 0x00bc6849:hlt
- 0x00bc684a:hlt
- 0x00bc684b:hlt
- 0x00bc684c:hlt
- 0x00bc684d:hlt
- 0x00bc684e:hlt
- 0x00bc684f:hlt
- [ExceptionHandler]
- [StubCode]
- 0x00bc6850:mov$0xdead,%ebx;{no_reloc}
- 0x00bc6855:mov$0xdead,%ecx
- 0x00bc685a:mov$0xdead,%edx
- 0x00bc685f:mov$0xdead,%esi
- 0x00bc6864:mov$0xdead,%edi
- 0x00bc6869:jmp0x00bc1d60;{runtime_call}
- 0x00bc686e:push$0xbc686e;{section_word}
- 0x00bc6873:jmp0x00b7ba40;{runtime_call}
上面位於0x00bc6820和0x00bc6826的兩條指令就是無限迴圈的殘餘物:
- 0x00bc6820:test%eax,0x970100;{poll}
- ;;26branch[AL][B0]
- 0x00bc6826:jmp0x00bc6820;*goto
- ;-TestC2InfiniteLoop::foo@2(line5)
原本在迴圈內的程式碼(int i = 1;)已經消失了,剩下的是在回邊處對safepoint的輪詢(test),以及迴圈末尾的無條件跳轉(jmp)。
從編譯記錄看,int i = 1;對應的程式碼是在暫存器分配過程中被削除的。
2、server模式:執行命令:
- java-server-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop
C2會拒絕編譯這種程式碼,並打出日誌:
- 1%TestC2InfiniteLoop::foo@0(5bytes)
- 1COMPILESKIPPED:trivialinfiniteloop(notretryable)
然後foo()中的無限迴圈就一直在直譯器中執行下去了。
在hotspot/src/share/vm/opto/compile.cpp中,bool Compile::final_graph_reshaping()的程式碼前面有這樣的註釋:
- //(4)Detectinfiniteloops;blobsofcodereachablefromabovebutnot
- //below.SeveraloftheCode_Genalgorithmsfailonsuchcodeshapes,
- //sowesimplybailout.HappensalotinZKM.jar,butalsohappens
- //fromtimetotimeinothercodes(suchas-Xcompfinalizerloops,etc).
- //DetectionisbylookingforIfNodeswhereonly1projectionis
- //reachablefrombeloworCatchNodesmissingsometargets.
- //...
- boolCompile::final_graph_reshaping(){
- //aninfiniteloopmayhavebeeneliminatedbytheoptimizer,
- //inwhichcasethegraphwillbeempty.
- if(root()->req()==1){
- record_method_not_compilable("trivialinfiniteloop");
- returntrue;
- }
- //...
- }
======================================================================
上面的實驗中,如果把foo中的無限迴圈在原始碼上就變為空迴圈的話:
- publicstaticvoidfoo(){
- while(true);
- }
則無論是在client還是server模式都不會觸發對該方法的JIT編譯,一直在直譯器中去執行那個迴圈。
檢視javac編譯生成的位元組碼,可以確認該迴圈在位元組碼中是存在的:
- publicstaticvoidfoo();
- Signature:()V
- Code:
- Stack=0,Locals=0,Args_size=0
- 0:goto0
- LineNumberTable:
- line5:0
- StackMapTable:number_of_entries=1
- frame_type=0/*same*/
foo()方法中唯一的一條位元組碼指令就是goto 0了。有趣的是因為foo()帶有無限迴圈,所以編譯出來的位元組碼裡連return都沒有。
HotSpot直譯器觸發JIT編譯,是通過兩個計數器來進行的:方法呼叫計數器與回邊計數器,當這兩個計數器的和超過了方法呼叫或迴圈次數的預設閾值就觸發對某個方法的JIT編譯;這兩個計數器在每個方法裡都有自己的一份。其中,方法呼叫計數器自然是用來記錄某個方法被呼叫的次數的,而回變計數器則用於記錄某方法中所有迴圈執行的次數(以方法而不是單個迴圈為粒度)。一個空的無限迴圈在位元組碼中的表現是一條goto位元組碼指令,引數是位元組碼的相對偏移量,並且該偏移量為0(意味著它指向該goto指令自身)。直譯器中有這樣的程式碼:
- 0x0097e781:test%edx,%edx
- 0x0097e783:jns0x0097e7a7
此時EDX持有相對偏移量,下面的JNS指令會在EDX為非負值的時候執行跳轉——正好跳過了計數器自增的程式碼;這樣就只有真正“向後跳”的goto才會使回邊計數器自增。也就是說空的無限迴圈不會引起回邊計數器的累加,預設配置下也就不會觸發HotSpot進行JIT編譯。
======================================================================
* 本文的測試環境是32位Windows XP SP3,E8400,Sun JDK 1.6.0 update 18
關於Java中的WeakReference
閱讀原文請訪問我的部落格BrightLoong's Blog
一. 簡介
在看ThreadLocal原始碼的時候,其中巢狀類ThreadLocalMap中的Entry繼承了WeakReferenc,為了能搞清楚ThreadLocal,只能先了解下了WeakReferenc(是的,很多時候為了搞清楚一個東西,不得不往上追好幾層,先搞清楚其所依賴的東西。)
下面進入正題,WeakReference如字面意思,弱引用, 當一個物件僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強引用)指向的時候, 如果這時GC執行, 那麼這個物件就會被回收,不論當前的記憶體空間是否足夠,這個物件都會被回收。
二. 認識WeakReference類
WeakReference繼承Reference,其中只有兩個建構函式:
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
- WeakReference(T referent):referent就是被弱引用的物件(注意區分弱引用物件和被弱引用的對應,弱引用物件是指WeakReference的例項或者其子類的例項),比如有一個Apple例項apple,可以如下使用,並且通過get()方法來獲取apple引用。也可以再建立一個繼承WeakReference的類來對Apple進行弱引用,下面就會使用這種方式。
WeakReference<Apple> appleWeakReference = new WeakReference<>(apple);
Apple apple2 = appleWeakReference.get();
- WeakReference(T referent, ReferenceQueue<? super T> q):與上面的構造方法比較,多了個ReferenceQueue,在物件被回收後,會把弱引用物件,也就是WeakReference物件或者其子類的物件,放入佇列ReferenceQueue中,注意不是被弱引用的物件,被弱引用的物件已經被回收了。
三. 使用WeakReference
下面是使用繼承WeakReference的方式來使用軟引用,並且不使用ReferenceQueue。
簡單類Apple
package io.github.brightloong.lab.reference;
/**
* Apple class
*
* @author BrightLoong
* @date 2018/5/25
*/
public class Apple {
private String name;
public Apple(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 覆蓋finalize,在回收的時候會執行。
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Apple: " + name + " finalize。");
}
@Override
public String toString() {
return "Apple{" +
"name='" + name + '\'' +
'}' + ", hashCode:" + this.hashCode();
}
}
繼承WeakReference的Salad
package io.github.brightloong.lab.reference;
import java.lang.ref.WeakReference;
/**
* Salad class
* 繼承WeakReference,將Apple作為弱引用。
* 注意到時候回收的是Apple,而不是Salad
*
* @author BrightLoong
* @date 2018/5/25
*/
public class Salad extends WeakReference<Apple> {
public Salad(Apple apple) {
super(apple);
}
}
Clent呼叫和輸出
package io.github.brightloong.lab.reference;
import java.lang.ref.WeakReference;
/**
* Main class
*
* @author BrightLoong
* @date 2018/5/24
*/
public class Client {
public static void main(String[] args) {
Salad salad = new Salad(new Apple("紅富士"));
//通過WeakReference的get()方法獲取Apple
System.out.println("Apple:" + salad.get());
System.gc();
try {
//休眠一下,在執行的時候加上虛擬機器引數-XX:+PrintGCDetails,輸出gc資訊,確定gc發生了。
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果為空,代表被回收了
if (salad.get() == null) {
System.out.println("clear Apple。");
}
}
}
輸出如下:
Apple:Apple{name='紅富士'}, hashCode:1846274136
[GC (System.gc()) [PSYoungGen: 3328K->496K(38400K)] 3328K->504K(125952K), 0.0035102 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(38400K)] [ParOldGen: 8K->359K(87552K)] 504K->359K(125952K), [Metaspace: 2877K->2877K(1056768K)], 0.0067965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Apple: 紅富士 finalize。
clear Apple。
ReferenceQueue的使用
package io.github.brightloong.lab.reference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* Client2 class
*
* @author BrightLoong
* @date 2018/5/27
*/
public class Client2 {
public static void main(String[] args) {
ReferenceQueue<Apple> appleReferenceQueue = new ReferenceQueue<>();
WeakReference<Apple> appleWeakReference = new WeakReference<Apple>(new Apple("青蘋果"), appleReferenceQueue);
WeakReference<Apple> appleWeakReference2 = new WeakReference<Apple>(new Apple("毒蘋果"), appleReferenceQueue);
System.out.println("=====gc呼叫前=====");
Reference<? extends Apple> reference = null;
while ((reference = appleReferenceQueue.poll()) != null ) {
//不會輸出,因為沒有回收被弱引用的物件,並不會加入佇列中
System.out.println(reference);
}
System.out.println(appleWeakReference);
System.out.println(appleWeakReference2);
System.out.println(appleWeakReference.get());
System.out.println(appleWeakReference2.get());
System.out.println("=====呼叫gc=====");
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=====gc呼叫後=====");
//下面兩個輸出為null,表示物件被回收了
System.out.println(appleWeakReference.get());
System.out.println(appleWeakReference2.get());
//輸出結果,並且就是上面的appleWeakReference、appleWeakReference2,再次證明物件被回收了
Reference<