1. 程式人生 > 實用技巧 >那些年被誤導的人們-java中將不再使用的區域性物件賦值為null,對垃圾回收有用嗎?

那些年被誤導的人們-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.

推薦閱讀(點選即可跳轉閱讀)

1.SpringBoot內容聚合

2.面試題內容聚合

3.設計模式內容聚合

4.Mybatis內容聚合

5.多執行緒內容聚合

釋出於 2019-11-14 from:https://zhuanlan.zhihu.com/p/91763061

java中將物件賦值為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. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args){
  4. foo();
  5. System.gc();
  6. }
  7. publicstaticvoidfoo(){
  8. byte[]placeholder=newbyte[64*1024*1024];
  9. }
  10. }

對應的輸出如下,可以看到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程式碼
  1. publicstaticvoidmain(String[]args)throwsException{
  2. byte[]placeholder=newbyte[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. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. byte[]placeholder=newbyte[64*1024*1024];
  5. placeholder=null;
  6. //dosometime-consumingoperation
  7. System.gc();
  8. }
  9. }

在解釋執行中,我們認為

寫道 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程式碼
  1. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. byte[]placeholder=newbyte[64*1024*1024];
  5. //dosometime-consumingoperation
  6. System.gc();
  7. }
  8. }

這樣的情況下,placeholder的物件是不會被回收的.可以理解..然後我們繼續修改方法體

Java程式碼
  1. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. {
  5. byte[]placeholder=newbyte[64*1024*1024];
  6. }
  7. System.gc();
  8. }
  9. }

我們執行發現

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

垃圾收集器並不會把物件給回收..明明已經出了作用域,竟然還是不回收!. 好吧,繼續修改例子

Java程式碼
  1. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. {
  5. byte[]placeholder=newbyte[64*1024*1024];
  6. }
  7. inta=0;
  8. System.gc();
  9. }
  10. }

唯一的變化就是新增了一個 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程式碼
  1. publicclassTest
  2. {
  3. publicvoidfoo(inta,intb){
  4. intc=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. publicclassTest
  2. {
  3. publicvoidfoo(inta,intb){
  4. {
  5. intd=0;
  6. }
  7. intc=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. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. {
  5. byte[]placeholder=newbyte[64*1024*1024];
  6. }
  7. System.gc();
  8. }
  9. }

這個例子中,在執行System.gc()的時候,雖然placeholder 的作用域已經結束,但是placeholder 對應的slot還存在,繼續持有64M陣列這個大物件,那麼自然的,在GC的時候不會把對應的大物件給清理掉.而在

Java程式碼
  1. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. {
  5. byte[]placeholder=newbyte[64*1024*1024];
  6. }
  7. inta=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. publicclassTest
  2. {
  3. publicstaticvoidmain(String[]args)throwsException{
  4. {
  5. intb=0;
  6. byte[]placeholder=newbyte[64*1024*1024];
  7. }
  8. inta=0;
  9. System.gc();
  10. }
  11. }

這個程式碼中,這個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程式碼
  1. publicclassTest
  2. {
  3. publicintadd(inta,intb){
  4. intc=a+1;
  5. returnc*b;
  6. }
  7. }
看javap輸出 首先要說明的是,按照之前的說法,方法引數會放到區域性變量表中.所以區域性變量表中的第一個值對應add方法的引數a,第二個值對應於引數b.

寫道 public int add(int, int);
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 -version
java 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:+UnlockDiagnosticV
MOptions -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程式碼
  1. 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程式碼
  1. publicclassTest{
  2. inta=1;
  3. staticintb=2;
  4. publicintsum(intc){
  5. returna+b+c;
  6. }
  7. publicstaticvoidmain(String[]args){
  8. newTest().sum(3);
  9. }
  10. }

執行的程式碼如下

寫道 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 output
VM 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程式碼
  1. //java-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop
  2. publicclassTestC2InfiniteLoop{
  3. publicstaticvoidfoo(){
  4. while(true){inti=1;}
  5. }
  6. publicstaticvoidmain(String[]args){
  7. foo();
  8. }
  9. }



HotSpot的JIT編譯器會:

1、client模式:執行命令:

Command prompt程式碼
  1. java-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop


C1會編譯程式碼,會將迴圈內無用的程式碼都消除掉,但不會把迴圈本身消除:

Hotspot log程式碼
  1. 1%TestC2InfiniteLoop::foo@0(5bytes)
  2. Decodingcompiledmethod0x00bc6748:
  3. Code:
  4. [Disassemblingformach=&apos;i386&apos;]
  5. [EntryPoint]
  6. [VerifiedEntryPoint]
  7. ;;blockB2[0,0]
  8. 0x00bc6810:mov%eax,-0x4000(%esp)
  9. 0x00bc6817:push%ebp
  10. 0x00bc6818:mov%esp,%ebp
  11. 0x00bc681a:sub$0x18,%esp;*iconst_1
  12. ;-TestC2InfiniteLoop::foo@0(line5)
  13. ;;blockB3[0,0]
  14. 0x00bc681d:nop
  15. 0x00bc681e:nop
  16. 0x00bc681f:nop;OopMap{off=16}
  17. ;*goto
  18. ;-TestC2InfiniteLoop::foo@2(line5)
  19. ;;blockB0[0,2]
  20. 0x00bc6820:test%eax,0x970100;{poll}
  21. ;;26branch[AL][B0]
  22. 0x00bc6826:jmp0x00bc6820;*goto
  23. ;-TestC2InfiniteLoop::foo@2(line5)
  24. ;;blockB1[0,0]
  25. 0x00bc6828:mov%eax,-0x4000(%esp)
  26. 0x00bc682f:push%ebp
  27. 0x00bc6830:mov%esp,%ebp
  28. 0x00bc6832:sub$0x18,%esp
  29. 0x00bc6835:mov%ecx,(%esp)
  30. 0x00bc6838:call0x082ea120;{runtime_call}
  31. ;;20branch[AL][B0]
  32. 0x00bc683d:jmp0x00bc6820
  33. 0x00bc683f:nop
  34. 0x00bc6840:nop
  35. 0x00bc6841:hlt
  36. 0x00bc6842:hlt
  37. 0x00bc6843:hlt
  38. 0x00bc6844:hlt
  39. 0x00bc6845:hlt
  40. 0x00bc6846:hlt
  41. 0x00bc6847:hlt
  42. 0x00bc6848:hlt
  43. 0x00bc6849:hlt
  44. 0x00bc684a:hlt
  45. 0x00bc684b:hlt
  46. 0x00bc684c:hlt
  47. 0x00bc684d:hlt
  48. 0x00bc684e:hlt
  49. 0x00bc684f:hlt
  50. [ExceptionHandler]
  51. [StubCode]
  52. 0x00bc6850:mov$0xdead,%ebx;{no_reloc}
  53. 0x00bc6855:mov$0xdead,%ecx
  54. 0x00bc685a:mov$0xdead,%edx
  55. 0x00bc685f:mov$0xdead,%esi
  56. 0x00bc6864:mov$0xdead,%edi
  57. 0x00bc6869:jmp0x00bc1d60;{runtime_call}
  58. 0x00bc686e:push$0xbc686e;{section_word}
  59. 0x00bc6873:jmp0x00b7ba40;{runtime_call}


上面位於0x00bc6820和0x00bc6826的兩條指令就是無限迴圈的殘餘物:

Hotspot log程式碼
  1. 0x00bc6820:test%eax,0x970100;{poll}
  2. ;;26branch[AL][B0]
  3. 0x00bc6826:jmp0x00bc6820;*goto
  4. ;-TestC2InfiniteLoop::foo@2(line5)


原本在迴圈內的程式碼(int i = 1;)已經消失了,剩下的是在回邊處對safepoint的輪詢(test),以及迴圈末尾的無條件跳轉(jmp)。
從編譯記錄看,int i = 1;對應的程式碼是在暫存器分配過程中被削除的。

2、server模式:執行命令:

Command prompt程式碼
  1. java-server-XX:+PrintCompilation-XX:+PrintAssemblyTestC2InfiniteLoop


C2會拒絕編譯這種程式碼,並打出日誌:

Hotspot log程式碼
  1. 1%TestC2InfiniteLoop::foo@0(5bytes)
  2. 1COMPILESKIPPED:trivialinfiniteloop(notretryable)


然後foo()中的無限迴圈就一直在直譯器中執行下去了。

在hotspot/src/share/vm/opto/compile.cpp中,bool Compile::final_graph_reshaping()的程式碼前面有這樣的註釋:

C++程式碼
  1. //(4)Detectinfiniteloops;blobsofcodereachablefromabovebutnot
  2. //below.SeveraloftheCode_Genalgorithmsfailonsuchcodeshapes,
  3. //sowesimplybailout.HappensalotinZKM.jar,butalsohappens
  4. //fromtimetotimeinothercodes(suchas-Xcompfinalizerloops,etc).
  5. //DetectionisbylookingforIfNodeswhereonly1projectionis
  6. //reachablefrombeloworCatchNodesmissingsometargets.
  7. //...
  8. boolCompile::final_graph_reshaping(){
  9. //aninfiniteloopmayhavebeeneliminatedbytheoptimizer,
  10. //inwhichcasethegraphwillbeempty.
  11. if(root()->req()==1){
  12. record_method_not_compilable("trivialinfiniteloop");
  13. returntrue;
  14. }
  15. //...
  16. }



======================================================================

上面的實驗中,如果把foo中的無限迴圈在原始碼上就變為空迴圈的話:

Java程式碼
  1. publicstaticvoidfoo(){
  2. while(true);
  3. }


則無論是在client還是server模式都不會觸發對該方法的JIT編譯,一直在直譯器中去執行那個迴圈。
檢視javac編譯生成的位元組碼,可以確認該迴圈在位元組碼中是存在的:

Javap程式碼
  1. publicstaticvoidfoo();
  2. Signature:()V
  3. Code:
  4. Stack=0,Locals=0,Args_size=0
  5. 0:goto0
  6. LineNumberTable:
  7. line5:0
  8. StackMapTable:number_of_entries=1
  9. frame_type=0/*same*/


foo()方法中唯一的一條位元組碼指令就是goto 0了。有趣的是因為foo()帶有無限迴圈,所以編譯出來的位元組碼裡連return都沒有。

HotSpot直譯器觸發JIT編譯,是通過兩個計數器來進行的:方法呼叫計數器與回邊計數器,當這兩個計數器的和超過了方法呼叫或迴圈次數的預設閾值就觸發對某個方法的JIT編譯;這兩個計數器在每個方法裡都有自己的一份。其中,方法呼叫計數器自然是用來記錄某個方法被呼叫的次數的,而回變計數器則用於記錄某方法中所有迴圈執行的次數(以方法而不是單個迴圈為粒度)。一個空的無限迴圈在位元組碼中的表現是一條goto位元組碼指令,引數是位元組碼的相對偏移量,並且該偏移量為0(意味著它指向該goto指令自身)。直譯器中有這樣的程式碼:

X86 asm程式碼
  1. 0x0097e781:test%edx,%edx
  2. 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<? extends Apple> reference2 = null;
        while ((reference2 = appleReferenceQueue.poll()) != null ) {
            //如果使用繼承的方式就可以包含其他資訊了
            System.out.println("appleReferenceQueue中:" + reference2);
        }
    }
}

結果輸出如下:

=====gc呼叫前=====
java.lang.ref.WeakReference@6e0be858
java.lang.ref.WeakReference@61bbe9ba
Apple{name='青蘋果'}, hashCode:1627674070
Apple{name='毒蘋果'}, hashCode:1360875712
=====呼叫gc=====
Apple: 毒蘋果 finalize。
Apple: 青蘋果 finalize。
=====gc呼叫後=====
null
null
appleReferenceQueue中:java.lang.ref.WeakReference@6e0be858
appleReferenceQueue中:java.lang.ref.WeakReference@61bbe9ba

Process finished with exit code 0

可以看到在佇列中(ReferenceQueue),呼叫gc之前是沒有內容的,呼叫gc之後,物件被回收了,並且弱引用物件appleWeakReference和appleWeakReference2被放入了佇列中。

關於其他三種引用,強引用、軟引用、虛引用,可以參考http://www.cnblogs.com/gudi/p/6403953.html

通過程式碼簡單介紹JDK 7的MethodHandle,並與.NET的委託對比

(注意:這篇blog裡的資訊只描述了發文時的情況。到JDK7正式釋出的時候已經這篇文章裡介紹的部分內容已經過時,包括MethodHandle的呼叫語法、java.dyn包改名為java.lang.invoke、該包的裡API等。下面內容並未更新,敬請理解。)

JDK 7將會實現JSR 292,為在JVM上實現動態語言提供更多支援。其中,MethodHandle是JSR 292的重要組成部分之一。有了它,意味著Java終於有了引用方法的方式,或者用C的術語說,“函式指標”。(我差點要說“引用‘方法’的‘方法’”了,好pun)。
下面的討論都是基於當前(2009-09)的設計而進行的,今後相關具體設計可能變化,但大的方向應該比較明確了。JDK 7的程式碼例子都是在JDK 7 Binary Snapshot build 70下測試的。執行程式時要新增-XX:+EnableMethodHandles引數。

與其說JDK 7的MethodHandle像C的函式指標,還不如說像.NET的委託。
C#與.NET從1.0版開始就有“委託”的概念,通過委託可以在程式碼中引用任意方法,無論方法的可訪問性、所屬型別如何,無論是靜態還是例項方法。之前一帖也提到了,.NET的委託提供了為方法建立“別名”的能力,使我們可以用統一的方式去呼叫簽名相同但名字和所屬型別都不一定相同的方法。與C的函式指標所不同的是,.NET的委託不但引用了方法,還會引用執行該方法所需要的環境,對引用例項方法的委託來說“環境”就是方法所在的例項;而C的函式指標則僅指向函式的程式碼而已,沒有引用環境的功能。而且,.NET的委託包含了足夠的元資料,可用於執行時做型別檢查;而C的函式指標則僅僅是個裸指標。呼叫委託的速度接近呼叫虛方法的速度。
JDK 7將引入的MethodHandle從許多方面說都與.NET的委託非常相似。MethodHandle也可以指向任意方法,提供為方法建立“別名”的能力;可以用統一的方式去呼叫MethodHandle。此外,MethodHandle還支援組合,可以以介面卡的方式將多個MethodHandle串在一起,實現引數過濾、引數轉換、返回值轉換等許多功能。呼叫MethodHandle的速度接近呼叫介面方法的速度。
MethodHandle對許多JVM的內部實現來說並不是一個全新的概念。要實現JVM,在內部總會保留一些指向方法的指標。JDK 7只是把它(和其它許多JVM裡原本就支援的概念)具體化為Java型別暴露給Java程式碼用而已;這就是所謂的“reification”。

好吧,簡單介紹了些背景,下面就通過程式碼來認識和感受一下JDK 7的MethodHandle,並與.NET的委託對比。

第一組例子,照例上hello world:

JDK 7:

Java程式碼
  1. importjava.dyn.*;
  2. importstaticjava.dyn.MethodHandles.*;
  3. publicclassTestMethodHandle1{
  4. privatestaticvoidhello(){
  5. System.out.println("Helloworld!");
  6. }
  7. publicstaticvoidmain(String[]args){
  8. MethodTypetype=MethodType.make(void.class);
  9. MethodHandlemethod=lookup()
  10. .findStatic(TestMethodHandle1.class,"hello",type);
  11. method.<void>invoke();
  12. }
  13. }


執行這個測試需要使用如下命令:

Command prompt程式碼
  1. java-XX:+EnableMethodHandlesTestMethodHandle1


(注意“java”要使用JDK 7的,不要用了JDK 6或更早的)

首先,要使用MethodHandle,需要引入的型別都在java.dyn包裡。這個例子用到的是MethodHandles、MethodHandles.Lookup、MethodType、MethodHandle幾個。
流程是:
0、呼叫MethodHandles.lookup()方法,遍歷呼叫棧檢查訪問許可權,然後得到一個MethodHandles.Lookup例項;該物件用於確認建立MethodHandle的例項的類對目標方法的訪問許可權是否滿足要求,並提供搜尋目標方法的邏輯;
1、指定目標方法的“方法型別”,得到一個MethodType例項;
2、通過MethodHandles.lookup()靜態方法得到一個型別為MethodHandles.Lookup的工廠,然後靠它搜尋指定的型別、指定的名字、指定的方法型別的方法,得到一個MethodHandle例項;
3、呼叫MethodHandle上的invoke方法。

其中,第1步中呼叫的MethodType.make()方法接收的引數是一組型別,第一個引數是返回型別,後面依次是各個引數的型別。上例中MethodType.make(void.class)得到的就是一個返回型別為void,引數列表為空的方法型別。如果熟悉Java位元組碼的話,這個方法型別的描述符就是()V。關於方法描述符的格式,可以參考JVM規範第二版4.3.3小節。MethodType的例項只代表所有返回值與引數型別匹配的一類方法的方法型別,自身沒有名字;在檢查某個方法是否與某個MethodType匹配時只考慮結構,可以算是一種特殊的structural-typing。

第2步看起來跟普通的反射很像,但通過反射得到的代表方法的物件是java.lang.reflect.Method的例項,它含有許多跟“執行”沒有直接關係的資訊,比較笨重;通過Method物件呼叫方法只是正常方法呼叫的模擬,所有引數會被包裝為一個數組,開銷較大。而MethodHandle則是個非常輕量的物件,主要目的就是用來引用方法並呼叫;通過它去呼叫方法不會導致引數被包裝,原始型別的引數也不會被自動裝箱。
MethodHandles.Lookup上有三個find方法,包括findStatic、findVirtual、findSpecial,分別對應invokestatic、invokevirtual/invokeinterface、invokespecial會對應的呼叫邏輯。注意到findVirtual方法所返回的MethodHandle的方法型別會包含一個顯式的“this”引數作為第一個引數;呼叫這樣的MethodHandle要顯式傳入“receiver”。這個看起來就跟.NET的開放委託相似,可以參考我之前的一帖。由於JDK 7的MethodHandle支援currying,可以把receiver儲存在MethodHandle裡,所以也可以創建出類似.NET的閉合委託的MethodHandle例項。
MethodHandles.Lookup上還有一組方法可以從通過反射API得到的Constructor、Field或Method物件創建出對應的MethodHandle。

第3步呼叫的MethodHandle.invoke()看似是一個虛方法,實際上並不是MethodHandle上真的存在的方法,而只是標記用的虛構出來的方法。上例中第13行對應的Java位元組碼是:

Java bytecode程式碼
  1. invokevirtualjava/dyn/MethodHandle.invoke:()V


也就是假裝MethodHandle上有一個描述符為()V且名為invoke的虛方法,通過invokevirtual指令去呼叫它。
Java編譯器為它做特殊處理:返回值型別如同泛型引數在<>內指定,不寫的話預設為返回Object型別;引數列表的型別則由Java編譯器根據實際引數的表示式推斷出來。與正常的泛型方法不同,MethodHandle.invoke指定返回值型別可以使用void和所有原始型別,不必像使用泛型方法時需要把原始型別寫為對應的包裝型別。
MethodHandle的方法型別不是Java語言的靜態型別系統的一部分。雖然它的例項在執行時帶有方法型別資訊(MethodType),但在編譯時Java編譯器卻不知道這一點。所以在編譯時,呼叫invoke時傳入任意個數、任意型別的引數都可以通過編譯;但在執行時要成功呼叫,由Java編譯器推斷出來的返回值型別與引數列表必須與執行時MethodHandle實際的方法型別一致,否則會丟擲WrongMethodTypeExceptionJohn Rose把MethodHandle.invoke的多型性稱為“簽名多型性(signature polymorphism)”。

使用者可以自行繼承java.dyn.JavaMethodHandle來建立自定義的MethodHandle子類,可以新增域或方法等,並可以指定該型別看作MethodHandle時的“入口點”——實際指向的方法。

許多JVM實現在JIT編譯的時候會做激進的優化,包括常量傳播、內聯、逃逸分析、無用程式碼削除等許多。JDK 7的MethodHandle的一個好處是它就像它所指向的目標方法的替身一樣,JVM原本可以做的優化對MethodHandle也一樣支援,特別是有需要的時候可以把目標方法內聯到呼叫處。相比之下,通過反射去呼叫方法則無法被JVM有效的優化。

對比C#的例子:
C# 2.0:

C#程式碼
  1. usingSystem;
  2. staticclassTestDelegate1{
  3. staticvoidHello(){
  4. Console.WriteLine("Helloworld!");
  5. }
  6. staticvoidMain(string[]args){
  7. Actionmethod=Hello;//Actionmethod=newAction(Hello);
  8. method();//method.Invoke();
  9. }
  10. }


這段程式碼與前面Java版的TestMethodHandle1功能基本相同。來觀察一下兩者的異同點。

要在C#裡使用委託的流程是:
0、事先宣告好合適的委託型別;
1、適用合適的委託型別,指定目標方法創建出委託的例項;
2、呼叫委託上的Invoke()方法。

.NET允許使用者自定義委託型別,在C#裡的語法是:

C#程式碼
  1. modifiersdelegatereturn_typeDelegateTypeName(argument_list);


該語法與方法的宣告語法非常像,只是在返回型別之前多了個delegate關鍵字而已,比C中typedef函式指標型別的語法容易多了。上例用到的System.Action型別就是標準庫裡宣告好的一個委託型別,其宣告形如:

C#程式碼
  1. namespaceSystem{
  2. publicdelegatevoidAction();
  3. }


這樣宣告出來的委託型別相當於聲明瞭一個Action類,繼承System.MulticastDelegate,並且擁有一個返回值型別為void,引數列表為空的Invoke()方法。在C#裡,使用者無法像宣告普通型別一樣通過宣告一個繼承System.Delegate或System.MulticastDelegate的類來得到一個新的委託型別,而只能用上述語法來宣告。不過從CLR的角度看,並沒有限制使用者不能自行繼承上述兩種型別來宣告新的委託型別。
委託是.NET型別系統的一部分。兩個委託型別即便表示的簽名一致也會被認為是不同的型別,不能相互賦值/轉換。這體現出了C#與.NET型別的nominal-typing性質。建立委託例項時則只考慮目標方法與委託型別在簽名上是否吻合,而不考慮名字問題,這點又與JDK 7的MethodHandle相似。
委託上的Invoke方法的簽名與委託宣告的相吻合。在編譯時,呼叫委託的Invoke()方法與呼叫一般的虛方法一樣會被型別檢查。
目前在C#裡可以顯式呼叫委託上的Invoke()方法,也可以直接把委託當成方法用括號呼叫。是顯式呼叫Invoke()還是直接用括號呼叫委託,現在來說只是程式設計師的偏好問題而已。事實上在C# 2.0以前編譯器會阻止程式設計師顯式呼叫Invoke()方法。

用C#程式碼例子再稍微解釋一下:

C#程式碼
  1. //下面宣告兩個委託型別,它們的簽名是一樣的
  2. //(int,int)->int
  3. delegateintBinaryIntOp1(intx,inty);
  4. delegateintBinaryIntOp2(inti,intj);
  5. //形式引數的型別是重要的,名字不重要
  6. //上面兩個委託上的Invoke()方法都形如:
  7. //[MethodImpl(0,MethodCodeType=MethodCodeType.Runtime)]
  8. //publicvirtualintInvoke(inti,intj);
  9. //注意到Invoke()是一個虛方法,其返回值與引數型別都是確定的,
  10. //而其實現是由執行時直接提供的。
  11. classDemo{
  12. //定義一個簽名為(int,int)->int的方法
  13. publicstaticintAdd(inta,intb){
  14. returna+b;
  15. }
  16. staticvoidMain(string[]args){
  17. //可以,Add方法與BinaryIntOp1要求的簽名匹配
  18. BinaryIntOp1op1=newBinaryIntOp1(Add);
  19. //可以,Add方法與BinaryIntOp1要求的簽名匹配
  20. //C#2.0的隱式建立委託的新特性:等同於寫成newBinaryIntOp2(Add);
  21. BinaryIntOp2op2=Add;
  22. //不行,兩者屬於不同的委託型別,無法相互賦值/轉換
  23. //BinaryIntOp2op3=op1;
  24. //呼叫Invoke()方法會被編譯器檢查型別是否匹配
  25. intsum=op1.Invoke(4,2);
  26. //下面這樣就會在編譯時出錯:
  27. //intsum2=op2.Invoke(newobject(),0);
  28. }
  29. }



Java版例子中,建立MethodHandle物件需要在程式碼裡通過MethodHandles.Lookup工廠來查詢目標;在C#裡編譯器已經幫忙找出了目標方法的token,寫起來方便許多;C#編譯器會根據方法名與委託的簽名去尋找合適過載版本的方法,找出它的token,並用於建立委託例項。如果目標方法無法在編譯時確定,使用System.Delegate.CreateDelegate(Type, MethodInfo)方法也可以依靠反射資訊創建出委託。

CLI的幾個主流實現,微軟的CLR、Novell的Mono等的JIT編譯器都比較靜態,遇到虛方法和委託呼叫都不會內聯。從這點說,.NET比JVM的技術複雜度要低一些。不過至少在.NET裡使用委託不會帶來多少額外開銷,所以還是可以放心使用的。

第二組例子,給hello world新增引數:

JDK 7:

Java程式碼
  1. importjava.dyn.*;
  2. importstaticjava.dyn.MethodHandles.*;
  3. publicclassTestMethodHandle2{
  4. privatestaticvoidhello(Stringname){
  5. System.out.printf("Hello,%s!\n",name);
  6. }
  7. publicstaticvoidmain(String[]args){
  8. if(0==args.length)args=newString[]{"Anonymous"};
  9. MethodTypetype=MethodType.make(void.class,String.class);
  10. MethodHandlemethod=lookup()
  11. .findStatic(TestMethodHandle2.class,"hello",type);
  12. method.<void>invoke(args[0]);
  13. }
  14. }


編譯,以下述命令執行

Command prompt程式碼
  1. java-XX:+EnableMethodHandlesTestMethodHandle2test


輸出結果為:

引用 Hello, test!


基本上跟第一組例子一樣,只是讓hello()多了個引數而已。留意一下建立MethodType例項的程式碼如何對應的改變。
第15行對應的Java位元組碼是:

Java bytecode程式碼
  1. invokevirtualjava/dyn/MethodHandle.invoke:(Ljava/lang/String;)V


留意Java編譯器是如何根據呼叫invoke時傳入的引數的靜態型別(編譯時型別)來決定invoke的方法描述符。(Ljava/lang/String;)V的意思是返回值型別為void,引數列表有一個引數,型別為java.lang.String。
如果把程式碼稍微修改,使MethodHandle的方法型別與Java編譯器推斷的呼叫型別不相符的話:

Java程式碼
  1. importjava.dyn.*;
  2. importstaticjava.dyn.MethodHandles.*;
  3. publicclassTestMethodHandle2{
  4. privatestaticvoidhello(Objectname){
  5. System.out.printf("Hello,%s!\n",name);
  6. }
  7. publicstaticvoidmain(String[]args){
  8. if(0==args.length)args=newString[]{"Anonymous"};
  9. MethodTypetype=MethodType.make(void.class,Object.class);
  10. MethodHandlemethod=lookup()
  11. .findStatic(TestMethodHandle2.class,"hello",type);
  12. method.<void>invoke(args[0]);
  13. }
  14. }


編譯執行會看到:

引用 Exception in thread "main" java.dyn.WrongMethodTypeException: (Ljava/lang/Object;)V cannot be called as (Ljava/lang/String;)V
at TestMethodHandle2.main(TestMethodHandle2.java:15)


這演示了Java編譯器將invoke的方法型別推斷為(Ljava/lang/String;)V,而被呼叫的MethodHandle例項實際的方法型別卻是(Ljava/lang/Object;)V,JVM便認為這個呼叫不匹配並拒絕執行。關鍵點是:呼叫invoke時,引數表示式的靜態型別(編譯時型別)必須與MethodHandle的方法型別中對於位置的引數型別“準確一致”;雖然String型別的引用可以隱式轉換為Object型別的,但不滿足“準確一致”的要求。
要想讓修改過的TestMethodHandle2再次正確執行,可以把第15行改為:method.<void>invoke((Object)args[0]);,也就是加個型別轉換,使Java編譯器推斷出來的方法描述符為(Ljava/lang/Object;)V。或者也可以加一個介面卡:

Java程式碼
  1. importjava.dyn.*;
  2. importstaticjava.dyn.MethodHandles.*;
  3. publicclassTestMethodHandle2{
  4. privatestaticvoidhello(Objectname){
  5. System.out.printf("Hello,%s!\n",name);
  6. }
  7. publicstaticvoidmain(String[]args){
  8. if(0==args.length)args=newString[]{"Anonymous"};
  9. MethodTypetype=MethodType.make(void.class,Object.class);
  10. MethodTypeadaptedType=MethodType.make(void.class,String.class);
  11. MethodHandlemethod=lookup()
  12. .findStatic(TestMethodHandle2.class,"hello",type);
  13. MethodHandleadaptedMethod=MethodHandles.convertArguments(
  14. method,adaptedType);
  15. adaptedMethod.<void>invoke(args[0]);
  16. }
  17. }


這裡演示了MethodHandle的可組裝性:通過給實際呼叫目標裝一個轉換引數型別的介面卡,方法呼叫就又可以成功了。

對比C#的例子:
C# 3.0:

C#程式碼
  1. usingSystem;
  2. staticclassTestDelegate2{
  3. staticvoidHello(stringname){
  4. Console.WriteLine("Hello,{0}!",name);
  5. }
  6. staticvoidMain(string[]args){
  7. if(0==args.Length)args=new[]{"Anonymous"};
  8. Action<string>method=Hello;
  9. method(args[0]);
  10. }
  11. }



基本上跟第一組例子也是一樣的。Action<T>是標準庫裡預先宣告好的一個泛型委託型別,其宣告形如:

C#程式碼
  1. namespaceSystem{
  2. publicdelegatevoidAction<T>(Tt);
  3. }


為了對比,下面也把Hello()的引數型別改為object,

C#程式碼
  1. usingSystem;
  2. staticclassTestDelegate2{
  3. staticvoidHello(objectname){
  4. Console.WriteLine("Hello,{0}!",name);
  5. }
  6. staticvoidMain(string[]args){
  7. if(0==args.Length)args=new[]{"Anonymous"};
  8. Action<object>method=Hello;
  9. method(args[0]);
  10. }
  11. }


編譯和執行都沒有任何問題。這裡要演示的是Invoke()方法是有確定的簽名的,與委託型別宣告的相吻合。編譯器不會擅自推斷Invoke()的簽名。

第三組例子,可指定排序條件的快速排序:
前面一直在用hello world作例子或許是無聊了點,下面弄點稍微長一些的。

JDK 7:

Java程式碼
  1. importjava.dyn.*;
  2. importjava.util.*;
  3. importstaticjava.dyn.MethodHandles.*;
  4. importstaticjava.lang.Integer.parseInt;
  5. publicclassTestMethodHandle3{
  6. privatestaticintcompareStringsByIntegerValue(Stringnum1,Stringnum2){
  7. returnparseInt(num1)-parseInt(num2);
  8. }
  9. privatestaticintcompareStringsByLength(Stringstr1,Stringstr2){
  10. returnstr1.length()-str2.length();
  11. }
  12. publicstaticvoidsort(String[]array,MethodHandlecomparer){
  13. if(0==array.length)return;
  14. sort(array,0,array.length-1,comparer);
  15. }
  16. privatestaticvoidsort(
  17. String[]array,
  18. intleft,
  19. intright,
  20. MethodHandlecomparer){
  21. if(left>=right)return;
  22. Stringpivot=array[right];
  23. intlo=left-1;
  24. inthi=right;
  25. while(true){
  26. while(comparer.<int>invoke(array[++lo],pivot)<0){
  27. }
  28. while(hi>left
  29. &&comparer.<int>invoke(array[--hi],pivot)>0){
  30. }
  31. if(lo>=hi)break;
  32. swap(array,lo,hi);
  33. }
  34. swap(array,lo,right);
  35. sort(array,left,lo-1,comparer);
  36. sort(array,lo+1,right,comparer);
  37. }
  38. privatestatic<E>voidswap(E[]array,inti,intj){
  39. Etemp=array[i];
  40. array[i]=array[j];
  41. array[j]=temp;
  42. }
  43. publicstaticvoidmain(String[]args){
  44. String[]array=newString[]{
  45. "25","02","250","48","0024","42","2"
  46. };
  47. MethodTypetype=MethodType.make(
  48. int.class,
  49. String.class,String.class);
  50. MethodHandlecomparer;
  51. comparer=lookup().findStatic(
  52. TestMethodHandle3.class,
  53. "compareStringsByIntegerValue",
  54. type);
  55. sort(array,comparer);
  56. for(Strings:array)System.out.println(s);
  57. System.out.println();
  58. comparer=lookup().findStatic(
  59. TestMethodHandle3.class,
  60. "compareStringsByLength",
  61. type);
  62. sort(array,comparer);
  63. for(Strings:array)System.out.println(s);
  64. }
  65. }


編譯,執行,輸出結果為:

引用 02
2
0024
25
42
48
250

2
25
42
48
02
250
0024



核心的sort()方法是個簡單的快排實現。為了讓使用者能夠指定排序條件,我讓它接收MethodHandle為引數來提供判斷邏輯。在沒有MethodHandle之前,我可能會選擇使用策略模式來達到剝離出部分演算法的目的。JDK裡的許多API也是這麼做的,例如Arrays.sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c),其中的Comparator引數就是“策略物件”。現在就可以直接用一個MethodHandle來代替它了。
例子的整體結構跟前兩組沒有顯著的不同,應該還是比較好理解的。
值得注意的是,我把swap()寫為了泛型方法,方便以後再更多地方能複用。排序也應該是個通用演算法,為何不把sort()也寫為泛型呢?
如果把例子中sort()方法裡出現的String全部直接替換為泛型的,變成:

Java程式碼
  1. publicstatic<E>voidsort(E[]array,MethodHandlecomparer){
  2. if(0==array.length)return;
  3. sort(array,0,array.length-1,comparer);
  4. }
  5. privatestatic<E>voidsort(
  6. E[]array,
  7. intleft,
  8. intright,
  9. MethodHandlecomparer){
  10. if(left>=right)return;
  11. Epivot=array[right];
  12. intlo=left-1;
  13. inthi=right;
  14. while(true){
  15. while(comparer.<int>invoke(array[++lo],pivot)<0){
  16. }
  17. while(hi>left
  18. &&comparer.<int>invoke(array[--hi],pivot)>0){
  19. }
  20. if(lo>=hi)break;
  21. swap(array,lo,hi);
  22. }
  23. swap(array,lo,right);
  24. sort(array,left,lo-1,comparer);
  25. sort(array,lo+1,right,comparer);
  26. }


編譯沒問題,但執行的時候會看到comparer.<int>invoke(...)的呼叫拋WrongMethodTypeException異常。你可能會納悶:array陣列的元素型別不是E麼,是泛型的啊;以String為泛型引數呼叫sort()的時候,invoke的實際引數型別與comparer的方法型別應該匹配才對,怎麼會出錯呢?
問題就在於Java的泛型是通過型別擦除法(type-erasure)來實現的,編譯過後所有泛型引數都變為了Object,這裡也不例外。所以Java編譯器推斷出來的invoke()的描述符是(Ljava/lang/Object;Ljava/lang/Object;)I,這就出現我們在第二組例子裡遇到的問題了——方法型別不準確匹配。
於是解決的辦法也很簡單,只要改成這樣:

Java程式碼
  1. publicstatic<E>voidsort(E[]array,MethodHandlecomparer){
  2. if(0==array.length)return;
  3. Class<Object>objClz=Object.class;
  4. MethodTypeadaptedType=MethodType.make(int.class,objClz,objClz);
  5. MethodHandleadaptedComparer=MethodHandles.convertArguments(
  6. comparer,adaptedType);
  7. sort(array,0,array.length-1,adaptedComparer);
  8. }


在呼叫核心的sort()方法前,先加一個介面卡來解決型別差異的問題,就萬事大吉了 ^ ^

對比C#的例子:
C# 2.0

C#程式碼
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. staticclassTestDelegate3{
  4. staticintCompareStringsByIntegerValue(stringnum1,stringnum2){
  5. returnConvert.ToInt32(num1)-Convert.ToInt32(num2);
  6. }
  7. staticintCompareStringsByLength(stringstr1,stringstr2){
  8. returnstr1.Length-str2.Length;
  9. }
  10. publicstaticvoidSort<T>(T[]array,Comparison<T>comparer){
  11. Sort(array,0,array.Length-1,comparer);
  12. }
  13. staticvoidSort<T>(
  14. T[]array,
  15. intleft,
  16. intright,
  17. Comparison<T>comparer){
  18. if(left>=right)return;
  19. Tpivot=array[right];
  20. intlo=left-1;
  21. inthi=right;
  22. while(true){
  23. while(comparer(array[++lo],pivot)<0){
  24. }
  25. while(hi>left
  26. &&comparer(array[--hi],pivot)>0){
  27. }
  28. if(lo>=hi)break;
  29. Swap(refarray[lo],refarray[hi]);
  30. }
  31. Swap(refarray[lo],refarray[right]);
  32. Sort(array,left,lo-1,comparer);
  33. Sort(array,lo+1,right,comparer);
  34. }
  35. staticvoidSwap<T>(refTt1,refTt2){
  36. Ttemp=t1;
  37. t1=t2;
  38. t2=temp;
  39. }
  40. staticvoidMain(string[]args){
  41. String[]array=newString[]{"25","02","250","48","0024","42","2"};
  42. Sort(array,CompareStringsByIntegerValue);
  43. foreach(varsinarray)Console.WriteLine(s);
  44. Console.WriteLine();
  45. Sort(array,CompareStringsByLength);
  46. foreach(varsinarray)Console.WriteLine(s);
  47. }
  48. }



基本上也沒什麼需要特別解釋的了。System.Comparison<T>是標準庫裡預先宣告的一個泛型委託型別,其宣告形如:

C#程式碼
  1. namespaceSystem{
  2. publicdelegateintComparison<T>(Tx,Ty);
  3. }


注意.NET裡委託與泛型可以很好的結合在一起使用,而不必考慮轉換引數型別的問題,因為.NET的泛型資訊會帶到執行時。或者說,“.NET generics are reified”。

如果用上C# 3.0和.NET Framework 3.5的新特性,使用lambda表示式來提供排序依據,

C#程式碼
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. staticclassTestDelegate3{
  4. publicstaticvoidSort<T>(T[]array,Func<T,T,int>comparer){
  5. Sort(array,0,array.Length-1,comparer);
  6. }
  7. staticvoidSort<T>(
  8. T[]array,
  9. intleft,
  10. intright,
  11. Func<T,T,int>comparer){
  12. if(left>=right)return;
  13. Tpivot=array[right];
  14. intlo=left-1;
  15. inthi=right;
  16. while(true){
  17. while(comparer(array[++lo],pivot)<0){
  18. }
  19. while(hi>left
  20. &&comparer(array[--hi],pivot)>0){
  21. }
  22. if(lo>=hi)break;
  23. Swap(refarray[lo],refarray[hi]);
  24. }
  25. Swap(refarray[lo],refarray[right]);
  26. Sort(array,left,lo-1,comparer);
  27. Sort(array,lo+1,right,comparer);
  28. }
  29. staticvoidSwap<T>(refTt1,refTt2){
  30. Ttemp=t1;
  31. t1=t2;
  32. t2=temp;
  33. }
  34. staticvoidMain(string[]args){
  35. vararray=new[]{"25","02","250","48","0024","42","2"};
  36. Sort(array,
  37. (s1,s2)=>Convert.ToInt32(s1)-Convert.ToInt32(s2));
  38. foreach(varsinarray)Console.WriteLine(s);
  39. Console.WriteLine();
  40. Sort(array,
  41. (s1,s2)=>s1.Length-s2.Length);
  42. foreach(varsinarray)Console.WriteLine(s);
  43. }
  44. }


我們就不必再為例中用到的排序條件寫具名方法了。實際上C#編譯器仍然為這兩個lambda表示式生成了私有靜態方法,我們得到的東西是一樣的(硬要說的話,少得到了倆名字),但需要寫在程式碼裡的東西減少了。
如果進一步改為使用標準庫中現成的方法來排序,

C#程式碼
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. staticclassTestDelegate3{
  4. staticvoidMain(string[]args){
  5. vararray=new[]{"25","02","250","48","0024","42","2"};
  6. Array.Sort(array,
  7. (s1,s2)=>Convert.ToInt32(s1)-Convert.ToInt32(s2));
  8. foreach(varsinarray)Console.WriteLine(s);
  9. Console.WriteLine();
  10. Array.Sort(array,
  11. (s1,s2)=>s1.Length-s2.Length);
  12. foreach(varsinarray)Console.WriteLine(s);
  13. }
  14. }


整個程式碼就簡潔了許多。


Alright,這帖就介紹MethodHandle與.NET的委託到這裡。希望上面的例子達到了簡單介紹JDK 7的MethodHandle的目的。
你可能會心存疑惑,“我為什麼需要引用別的方法呢?” 由於Java以前一直沒有提供這樣的功能,一直只使用Java的人可能較少思考這個問題。但你可能會碰到過這樣的問題,有時候要呼叫的目標只有到了執行的時候才知道,無法在程式碼裡直接寫方法呼叫,那怎麼辦?以前的解決辦法就只有通過反射了。一個最簡單的使用場景就是,原本通過反射做的方法呼叫,現在都可以通過MethodHandle完成。如果一個方法頻繁被反射呼叫,開銷會很明顯,而且難以優化;通過MethodHandle呼叫則跟正常的介面方法呼叫速度接近,沒有各種包裝/裝箱的開銷,而且可以被JVM優化,何樂而不為?
在MethodHandle出現之前,許多JVM上的指令碼語言實現,如JRuby,為了提高呼叫方法的速度,選擇生成大量很小的“invoker類”來對方法簽名做特化,通過它們去呼叫目標方法,避免反射呼叫的開銷。但這麼做有許多問題,一是實現麻煩,而是對PermGen堆帶來巨大的壓力。MethodHandle的出現極大的改善了狀況,既便於使用,效率又高,而且還不會對PermGen帶來多少壓力。Charles Nutter在一帖中描述了這個問題
編輯:經指正,Groovy當前還沒有用invoke stub,只是做了callsite caching優化。我只讀過Groovy的編譯器的程式碼,沒讀過執行時部分的程式碼,忽悠了同學們了,抱歉 <(_ _)>)

進一步介紹等以後寫invokedynamic的時候再寫了~ until then

Have fun ^ ^

[轉]Java 的強引用、弱引用、軟引用、虛引用

1、強引用(StrongReference)

強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。如下:

Object o=new Object();   //  強引用

當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。如果不使用時,要通過如下方式來弱化引用,如下:

o=null;     // 幫助垃圾收集器回收此物件

顯式地設定o為null,或超出物件的生命週期範圍,則gc認為該物件不存在引用,這時就可以回收這個物件。具體什麼時候收集這要取決於gc的演算法。

舉例:

public void test(){
    Object o=new Object();
    // 省略其他操作
}

在一個方法的內部有一個強引用,這個引用儲存在棧中,而真正的引用內容(Object)儲存在堆中。當這個方法執行完成後就會退出方法棧,則引用內容的引用不存在,這個Object會被回收。

但是如果這個o是全域性的變數時,就需要在不用這個物件時賦值為null,因為強引用不會被垃圾回收。

強引用在實際中有非常重要的用處,舉個ArrayList的實現原始碼:

private transient Object[] elementData;
public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
}

在ArrayList類中定義了一個私有的變數elementData陣列,在呼叫方法清空陣列時可以看到為每個陣列內容賦值為null。不同於elementData=null,強引用仍然存在,避免在後續呼叫 add()等方法新增元素時進行重新的記憶體分配。使用如clear()方法中釋放記憶體的方法對陣列中存放的引用型別特別適用,這樣就可以及時釋放記憶體。

2軟引用(SoftReference)

如果一個物件只具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體。

String str=new String("abc");                                     // 強引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 軟引用

當記憶體不足時,等價於:

If(JVM.記憶體不足()) {
   str = null;  // 轉換為軟引用
   System.gc(); // 垃圾回收器進行回收
}

軟引用在實際中有重要的應用,例如瀏覽器的後退按鈕。按後退時,這個後退時顯示的網頁內容是重新進行請求還是從快取中取出呢?這就要看具體的實現策略了。

(1)如果一個網頁在瀏覽結束時就進行內容的回收,則按後退檢視前面瀏覽過的頁面時,需要重新構建

(2)如果將瀏覽過的網頁儲存到記憶體中會造成記憶體的大量浪費,甚至會造成記憶體溢位

這時候就可以使用軟引用

Browser prev = new Browser();               // 獲取頁面進行瀏覽
SoftReference sr = new SoftReference(prev); // 瀏覽完畢後置為軟引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 還沒有被回收器回收,直接獲取
}else{
    prev = new Browser();               // 由於記憶體吃緊,所以對軟引用的物件回收了
    sr = new SoftReference(prev);       // 重新構建
}

這樣就很好的解決了實際的問題。

軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

3、弱引用(WeakReference)

弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

String str=new String("abc");    
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
str=null;

當垃圾回收器進行掃描回收時等價於:

str = null;
System.gc();

如果這個物件是偶爾的使用,並且希望在使用時隨時就能獲取到,但又不想影響此物件的垃圾收集,那麼你應該用 Weak Reference 來記住此物件。

下面的程式碼會讓str再次變為一個強引用:

String  abc = abcWeakRef.get();

弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

當你想引用一個物件,但是這個物件有自己的生命週期,你不想介入這個物件的生命週期,這時候你就是用弱引用。

這個引用不會在物件的垃圾回收判斷中產生任何附加的影響

public class ReferenceTest {

    private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();

    public static void checkQueue() {
        Reference<? extends VeryBig> ref = null;
        while ((ref = rq.poll()) != null) {
            if (ref != null) {
                System.out.println("In queue: "    + ((VeryBigWeakReference) (ref)).id);
            }
        }
    }

    public static void main(String args[]) {
        int size = 3;
        LinkedList<WeakReference<VeryBig>> weakList = new LinkedList<WeakReference<VeryBig>>();
        for (int i = 0; i < size; i++) {
            weakList.add(new VeryBigWeakReference(new VeryBig("Weak " + i), rq));
            System.out.println("Just created weak: " + weakList.getLast());

        }

        System.gc(); 
        try { // 下面休息幾分鐘,讓上面的垃圾回收執行緒執行完成
            Thread.currentThread().sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        checkQueue();
    }
}

class VeryBig {
    public String id;
    // 佔用空間,讓執行緒進行回收
    byte[] b = new byte[2 * 1024];

    public VeryBig(String id) {
        this.id = id;
    }

    protected void finalize() {
        System.out.println("Finalizing VeryBig " + id);
    }
}

class VeryBigWeakReference extends WeakReference<VeryBig> {
    public String id;

    public VeryBigWeakReference(VeryBig big, ReferenceQueue<VeryBig> rq) {
        super(big, rq);
        this.id = big.id;
    }

    protected void finalize() {
        System.out.println("Finalizing VeryBigWeakReference " + id);
    }
}

最後的輸出結果為:

Just created weak: com.javabase.reference.VeryBigWeakReference@1641c0
Just created weak: com.javabase.reference.VeryBigWeakReference@136ab79
Just created weak: com.javabase.reference.VeryBigWeakReference@33c1aa
Finalizing VeryBig Weak 2
Finalizing VeryBig Weak 1
Finalizing VeryBig Weak 0
In queue: Weak 1
In queue: Weak 2
In queue: Weak 0

4、虛引用(PhantomReference)

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

虛引用主要用來跟蹤物件被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之 關聯的引用佇列中。

5、總結

Java4種引用的級別由高到低依次為:

強引用 > 軟引用 > 弱引用 > 虛引用

通過圖來看一下他們之間在垃圾回收時的區別:

當垃圾回收器回收時,某些物件會被回收,某些不會被回收。垃圾回收器會從根物件Object來標記存活的物件,然後將某些不可達的物件和一些引用的物件進行回收,如果對這方面不是很瞭解,可以參考如下的文章:

通過表格來說明一下,如下:

參考文獻:

1、http://www.cnblogs.com/skywang12345/p/3154474.html

2、http://blog.csdn.net/lifetragedy?viewmode=contents

大物件簡介+大物件的4種類型+lob型別的優點+lob的組成

資料庫

大物件簡介
1用來儲存大型資料,如圖片,視訊,音樂等
2可用於儲存二進位制資料,字元資料,引用外部檔案的指標的資料型別

大物件的4種類型
1BLOB資料型別
1)它是用來儲存二進位制資料。
2)可以儲存的最大資料量是(4GB-1)*db_block_size(最大32kb),也就是128TB.

2CLOB資料型別
1)儲存字元資料
2)可以儲存的最大資料量是(4GB-1)*db_block_size(最大32kb),也就是128TB.

3NCLOB資料型別
1)用來儲存多位元組字元的資料,一般用於非英文的字元
2)可以儲存的最大資料量是(4GB-1)*db_block_size(最大32kb),也就是128TB.
4是BFILE資料型別
1)儲存檔案指標。
2)資料檔案可以儲存在資料庫之外,資料庫只儲存對該檔案的引用。
3)其最多也可以儲存4GB的資料。

lob型別的優點
1lob列最大可以儲存128TB的資料,遠遠超過long和long row列儲存的資料量(2GB)
2一個表可以多個lob列,但是隻能有一個long或long raw列
3lob資料可以隨機訪問,long和long raw 資料只能順序訪問。
lob的組成
1lob定位器:一個子針,指定lob資料的位置
2lob資料:儲存在lob中的實際資料
注意
1當blob,clob,nclob資料列的資料小於4KB,則lob資料儲存在表中,否則lob資料儲存在表外。
2bfile資料列,只有lob定位器儲存在表中