1. 程式人生 > >優秀Java程式設計師應該瞭解的GC工作原理

優秀Java程式設計師應該瞭解的GC工作原理

一個優秀的Java程式設計師必須瞭解GC的工作原理、如何優化GC的效能、如何與GC進行有限的互動,因為有一些應用程式對效能要求較高,例如嵌入式系統、實時系統等,只有全面提升記憶體的管理效率 ,才能提高整個應用程式的效能。


一個優秀的Java程式設計師必須瞭解GC的工作原理、如何優化GC的效能、如何與GC進行有限的互動,因為有一些應用程式對效能要求較高,例如嵌入式系統、實時系統等,只有全面提升記憶體的管理效率 ,才能提高整個應用程式的效能。本篇文章首先簡單介紹GC的工作原理之後,然後再對GC的幾個關鍵問題進行深入探討,最後提出一些Java程式設計建議,從GC角度提高Java程式的效能。

GC的基本原理

 

Java的記憶體管理實際上就是物件的管理,其中包括物件的分配和釋放。

對於程式設計師來說,分配物件使用new關鍵字;釋放物件時,只要將物件所有引用賦值為null,讓程式不能夠再訪問到這個物件,我們稱該物件為"不可達的".GC將負責回收所有"不可達"物件的記憶體空間。

對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有物件。通過這種方式確定哪些物件是"可達的",哪些物件是"不可達的".當GC確定一些物件為"不可達"時,GC就有責任回收這些記憶體空間。

 

但是,為了保證GC能夠在不同平臺實現的問題,Java規範對GC的很多行為都沒有進行嚴格的規定。例如,對於採用什麼型別的回收演算法、什麼時候進行回收等重要問題都沒有明確的規定。因此,不同的JVM的實現者往往有不同的實現演算法。這也給Java程式設計師的開發帶來行多不確定性。本文研究了幾個與GC工作相關的問題,努力減少這種不確定性給Java程式帶來的負面影響。

增量式GC( Incremental GC )

GC在JVM中通常是由一個或一組程序來實現的,它本身也和使用者程式一樣佔用heap空間,執行時也佔用CPU.當GC程序執行時,應用程式停止執行。因此,當GC執行時間較長時,使用者能夠感到 Java程式的停頓,另外一方面,如果GC執行時間太短,則可能物件回收率太低,這意味著還有很多應該回收的物件沒有被回收,仍然佔用大量記憶體。

 

因此,在設計GC的時候,就必須在停頓時間和回收率之間進行權衡。一個好的GC實現允許使用者定義自己所需要的設定,例如有些記憶體有限有裝置,對記憶體的使用量非常敏感,希望GC能夠準確的回收記憶體,它並不在意程式速度的放慢。

 

另外一些實時網路遊戲,就不能夠允許程式有長時間的中斷。增量式GC就是通過一定的回收演算法,把一個長時間的中斷,劃分為很多個小的中斷,通過這種方式減少GC對使用者程式的影響。雖然,增量式GC在整體效能上可能不如普通GC的效率高,但是它能夠減少程式的最長停頓時間。

Sun JDK提供的HotSpot JVM就能支援增量式GC.HotSpot JVM預設GC方式為不使用增量GC,為了啟動增量GC,我們必須在執行Java程式時增加-Xincgc的引數。

 

HotSpot JVM增量式GC的實現是採用Train GC演算法。它的基本想法就是,將堆中的所有物件按照建立和使用情況進行分組(分層),將使用頻繁高和具有相關性的物件放在一隊中,隨著程式的執行,不斷對組進行調整。當GC執行時,它總是先回收最老的(最近很少訪問的)的物件,如果整組都為可回收物件,GC將整組回收。這樣,每次GC執行只回收一定比例的不可達物件,保證程式的順暢執行。

詳解finalize函式

finalize是位於Object類的一個方法,該方法的訪問修飾符為protected,由於所有類為Object的子類,因此使用者類很容易訪問到這個方法。由於,finalize函式沒有自動實現鏈式呼叫,我們必須手動的實現,因此finalize函式的最後一個語句通常是super.finalize()。通過這種方式,我們可以實現從下到上實現finalize的呼叫,即先釋放自己的資源,然後再釋放父類的資源。

根據Java語言規範,JVM保證呼叫finalize函式之前,這個物件是不可達的,但是JVM不保證這個函式一定會被呼叫。另外,規範還保證finalize函式最多執行一次。

很多Java初學者會認為這個方法類似與C++中的解構函式,將很多物件、資源的釋放都放在這一函式裡面。其實,這不是一種很好的方式。原因有三,其一,GC為了能夠支援finalize函式,要對覆蓋這個函式的物件作很多附加的工作。其二,在finalize執行完成之後,該物件可能變成可達的,GC還要再檢查一次該物件是否是可達的。因此,使用 finalize會降低GC的執行效能。其三,由於GC呼叫finalize的時間是不確定的,因此通過這種方式釋放資源也是不確定的。

通常,finalize用於一些不容易控制、並且非常重要資源的釋放,例如一些I/O的操作,資料的連線。這些資源的釋放對整個應用程式是非常關鍵的。在這種情況下,程式設計師應該以通過程式本身管理(包括釋放)這些資源為主,以finalize函式釋放資源方式為輔,形成一種雙保險的管理機制,而不應該僅僅依靠finalize來釋放資源。

下面給出一個例子說明,finalize函式被呼叫以後,仍然可能是可達的,同時也可說明一個物件的finalize只可能執行一次。

 

  1 class MyObject{
 2 
 3 Test main; //記錄Test物件,在finalize中時用於恢復可達性
 4 
 5 public MyObject(Test t)
 6 
 7 {
 8 
 9 main=t; //儲存Test 物件
10 
11 }
12 
13 protected void finalize()
14 
15 {
16 
17 main.ref=this;// 恢復本物件,讓本物件可達
18 
19 System.out.println("This is finalize");//用於測試finalize只執行一次
20 
21 }
22 
23 }
24 
25 class Test {
26 
27 MyObject ref;
28 
29 public static void main(String[] args) {
30 
31 Test test=new Test();
32 
33 test.ref=new MyObject(test);
34 
35 test.ref=null; //MyObject物件為不可達物件,finalize將被呼叫
36 
37 System.gc();
38 
39 if (test.ref!=null) System.out.println("My Object還活著");
40 
41 }
42 
43 }
44 
45 執行結果:
46 
47 This is finalize
48 
49 MyObject還活著

 

此例子中,需要注意的是雖然MyObject物件在finalize中變成可達物件,但是下次回收時候,finalize卻不再被呼叫,因為finalize函式最多隻呼叫一次。

程式如何與GC進行互動

Java2增強了記憶體管理功能,增加了一個java.lang.ref包,其中定義了三種引用類。這三種引用類分別為SoftReference、WeakReference和 PhantomReference.通過使用這些引用類,程式設計師可以在一定程度與GC進行互動,以便改善GC的工作效率。這些引用類的引用強度介於可達物件和不可達物件之間。

建立一個引用物件也非常容易,例如如果你需要建立一個Soft Reference物件,那麼首先建立一個物件,並採用普通引用方式(可達物件);然後再建立一個SoftReference引用該物件;最後將普通引用設定為null.通過這種方式,這個物件就只有一個Soft Reference引用。同時,我們稱這個物件為Soft Reference 物件。

Soft Reference的主要特點是據有較強的引用功能。只有當記憶體不夠的時候,才進行回收這類記憶體,因此在記憶體足夠的時候,它們通常不被回收。另外,這些引用物件還能保證在Java丟擲OutOfMemory 異常之前,被設定為null.它可以用於實現一些常用圖片的快取,實現Cache的功能,保證最大限度的使用記憶體而不引起OutOfMemory.以下給出這種引用型別的使用虛擬碼;

 

 1 //申請一個影象物件
 2 
 3 Image image=new Image();//建立Image物件
 4 
 5 …
 6 
 7 //使用 image
 8 
 9 …
10 
11 //使用完了image,將它設定為soft 引用型別,並且釋放強引用;
12 
13 SoftReference sr=new SoftReference(image);
14 
15 image=null;
16 
17 …
18 
19 //下次使用時
20 
21 if (sr!=null) image=sr.get();
22 
23 else{
24 
25 //由於GC由於低記憶體,已釋放image,因此需要重新裝載;
26 
27 image=new Image();
28 
29 sr=new SoftReference(image);
30 
31 }

 

Weak引用物件與Soft引用物件的最大不同就在於:GC在進行回收時,需要通過演算法檢查是否回收Soft引用物件,而對於Weak引用物件,GC總是進行回收。Weak引用物件更容易、更快被 GC回收。雖然,GC在執行時一定回收Weak物件,但是複雜關係的Weak物件群常常需要好幾次GC的執行才能完成。Weak引用物件常常用於Map結構中,引用資料量較大的物件,一旦該物件的強引用為null時,GC能夠快速地回收該物件空間。

Phantom引用的用途較少,主要用於輔助 finalize函式的使用。Phantom物件指一些物件,它們執行完了finalize函式,併為不可達物件,但是它們還沒有被GC回收。這種物件可以輔助finalize進行一些後期的回收工作,我們通過覆蓋Reference的clear()方法,增強資源回收機制的靈活性。

一些Java編碼的建議

根據GC的工作原理,我們可以通過一些技巧和方式,讓GC執行更加有效率,更加符合應用程式的要求。以下就是一些程式設計的幾點建議。

1.最基本的建議就是儘早釋放無用物件的引用。大多數程式設計師在使用臨時變數的時候,都是讓引用變數在退出活動域(scope)後,自動設定為null.我們在使用這種方式時候,必須特別注意一些複雜的物件圖,例如陣列,佇列,樹,圖等,這些物件之間有相互引用關係較為複雜。對於這類物件,GC回收它們一般效率較低。如果程式允許,儘早將不用的引用物件賦為null.這樣可以加速GC的工作。

2.儘量少用finalize函式。finalize函式是Java提供給程式設計師一個釋放物件或資源的機會。但是,它會加大GC的工作量,因此儘量少採用finalize方式回收資源。

3.如果需要使用經常使用的圖片,可以使用soft應用型別。它可以儘可能將圖片儲存在記憶體中,供程式呼叫,而不引起OutOfMemory.

4.注意集合資料型別,包括陣列,樹,圖,連結串列等資料結構,這些資料結構對GC來說,回收更為複雜。另外,注意一些全域性的變數,以及一些靜態變數。這些變數往往容易引起懸掛物件(dangling reference),造成記憶體浪費。

5.當程式有一定的等待時間,程式設計師可以手動執行System.gc(),通知GC執行,但是Java語言規範並不保證GC一定會執行。使用增量式GC可以縮短Java程式的暫停時間。

歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 854393687
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!