Java GC回收機制

優秀Java程序員必須瞭解的GC工做原理html

 

一個優秀的Java程序員必須瞭解GC的工做原理、如何優化GC的性能、如何與GC進行有限的交互,由於有一些應用程序對性能要求較高,例如嵌入式系統、實時系統等,只有全面提高內存的管理效率 ,才能提升整個應用程序的性能。

一個優秀的Java程序員必須瞭解GC的工做原理、如何優化GC的性能、如何與GC進行有限的交互,由於有一些應用程序對性能要求較高,例如嵌入式系統、實時系統等,只有全面提高內存的管理效率 ,才能提升整個應用程序的性能。本篇文章首先簡單介紹GC的工做原理以後,而後再對GC的幾個關鍵問題進行深刻探討,最後提出一些Java程序設計建議,從GC角度提升Java程序的性能。

GC的基本原理

Java的內存管理實際上就是對象的管理,其中包括對象的分配和釋放。

對於程序員來講,分配對象使用new關鍵字;釋放對象時,只要將對象全部引用賦值爲null,讓程序不可以再訪問到這個對象,咱們稱該對象爲\"不可達的\".GC將負責回收全部\"不可達\"對象的內存空間。

對於GC來講,當程序員建立對象時,GC就開始監控這個對象的地址、大小以及使用狀況。一般,GC採用有向圖的方式記錄和管理堆(heap)中的全部對象(詳見 參考資料1 )。經過這種方式肯定哪些對象是\"可達的\",哪些對象是\"不可達的\".當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只可能運行一次。java

複製代碼
 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程序的暫停時間。算法

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

 

 

目錄編程

  1. Java垃圾回收概況
  2. Java內存區域
  3. Java對象的訪問方式
  4. Java內存分配機制
  5. Java GC機制
  6. 垃圾收集器

Java垃圾回收概況數組

  Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,做爲Java開發者,通常不須要專門編寫內存回收和垃圾清理代 碼,對內存泄露和溢出的問題,也不須要像C程序員那樣戰戰兢兢。這是由於在Java虛擬機中,存在自動內存管理和垃圾清掃機制。歸納地說,該機制對 JVM(Java Virtual Machine)中的內存進行標記,並肯定哪些內存須要回收,根據必定的回收策略,自動的回收內存,永不停息(Nerver Stop)的保證JVM中的內存空間,放置出現內存泄露和溢出問題。緩存

  關於JVM,須要說明一下的是,目前使用最多的Sun公司的JDK中,自從 1999年的JDK1.2開始直至如今仍在普遍使用的JDK6,其中默認的虛擬機都是HotSpot。2009年,Oracle收購Sun,加上以前收購 的EBA公司,Oracle擁有3大虛擬機中的兩個:JRockit和HotSpot,Oracle也代表了想要整合兩大虛擬機的意圖,可是目前在新發布 的JDK7中,默認的虛擬機仍然是HotSpot,所以本文中默認介紹的虛擬機都是HotSpot,相關機制也主要是指HotSpot的GC機制。網絡

  Java GC機制主要完成3件事:肯定哪些內存須要回收,肯定何時須要執行GC,如何執行GC。通過這麼長時間的發展(事實上,在Java語言出現以前,就有 GC機制的存在,如Lisp語言),Java GC機制已經日臻完善,幾乎能夠自動的爲咱們作絕大多數的事情。然而,若是咱們從事較大型的應用軟件開發,曾經出現過內存優化的需求,就一定要研究 Java GC機制。數據結構

  學習Java GC機制,能夠幫助咱們在平常工做中排查各類內存溢出或泄露問題,解決性能瓶頸,達到更高的併發量,寫出更高效的程序。多線程

  咱們將從4個方面學習Java GC機制,1,內存是如何分配的;2,如何保證內存不被錯誤回收(即:哪些內存須要回收);3,在什麼狀況下執行GC以及執行GC的方式;4,如何監控和優化GC機制。

Java內存區域

  瞭解Java GC機制,必須先清楚在JVM中內存區域的劃分。在Java運行時的數據區裏,由JVM管理的內存區域分爲下圖幾個模塊:

其中:

1,程序計數器(Program Counter Register):程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到了第幾行,能夠理解爲是當前線程的行號指示器。字節碼解釋器在工做時,會經過改變這個計數器的值來取下一條語句指令。

  每一個程序計數器只用來記錄一個線程的行號,因此它是線程私有(一個線程就有一個程序計數器)的。

  若是程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;若是正在執行的是一個本地(native,由C語言編寫 完成)方法,則計數器的值爲Undefined,因爲程序計數器只是記錄當前指令地址,因此不存在內存溢出的狀況,所以,程序計數器也是全部JVM內存區 域中惟一一個沒有定義OutOfMemoryError的區域。

2,虛擬機棧(JVM Stack):一個線程的每一個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操做站、動態連接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。

  局部變量表中存儲着方法的相關局部變量,包括各類基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會佔 用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。須要注意的是,局部變量表是在編譯時就已經肯定 好的,方法運行所須要分配的空間在棧幀中是徹底肯定的,在方法的生命週期內都不會改變。

  虛擬機棧中定義了兩種異常,若是線程調用的棧深度大於虛擬機容許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多 數Java虛擬機都容許動態擴展虛擬機棧的大小(有少部分是固定長度的),因此線程能夠一直申請棧,知道內存不足,此時,會拋出 OutOfMemoryError(內存溢出)。

  每一個線程對應着一個虛擬機棧,所以虛擬機棧也是線程私有的。

3,本地方法棧(Native Method Statck):本地方法棧在做用,運行機制,異常類型等方面都與虛擬機棧相同,惟一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在不少虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一塊兒使用。

  本地方法棧也是線程私有的。

4,堆區(Heap):堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由全部線程共享,在虛擬機啓動時建立。堆區的存在是爲了存儲對象實例,原則上講,全部的對象都在堆區上分配內存(不過現代技術裏,也不是這麼絕對的,也有棧上直接分配的)。

  通常的,根據Java虛擬機規範規定,堆內存須要在邏輯上是連續的(在物理上不須要),在實現時,能夠是固定大小的,也能夠是可擴展的,目前主 流的虛擬機都是可擴展的。若是在執行垃圾回收以後,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。

  關於堆區的內容還有不少,將在下節「Java內存分配機制」中詳細介紹。

5,方法區(Method Area):在Java虛擬機規範中,將方法區做爲堆的一個邏輯部分來對待,但事實 上,方法區並非堆(Non-Heap);另外,很多人的博客中,將Java GC的分代收集機制分爲3個代:青年代,老年代,永久代,這些做者將方法區定義爲「永久代」,這是由於,對於以前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,並將方法區設計成了永久代。不過,除HotSpot以外的多數虛擬機,並不將方法區當作永 久代,HotSpot自己,也計劃取消永久代。本文中,因爲筆者主要使用Oracle JDK6.0,所以仍將使用永久代一詞。

  方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時須要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

  方法區在物理上也不須要是連續的,能夠選擇固定大小或可擴展大小,而且方法區比堆還多了一個限制:能夠選擇是否執行垃圾收集。通常的,方法區上 執行的垃圾收集是不多的,這也是方法區被稱爲永久代的緣由之一(HotSpot),但這也不表明着在方法區上徹底沒有垃圾收集,其上的垃圾收集主要是針對 常量池的內存回收和對已加載類的卸載。

  在方法區上進行垃圾收集,條件苛刻並且至關困難,效果也不使人滿意,因此通常不作太多考慮,能夠留做之後進一步深刻研究時使用。

  在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。

  運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也能夠存儲在運行時間產生的常量(好比String類的intern()方法,做用是String維護了一個常量池,若是調用的字符「abc」已經在常量池中,則返回池中的字符串地址,不然,新建一個常量加入池中,並返回地址)。

6,直接內存(Direct Memory):直接內存並非JVM管理的內存,能夠這樣理解,直接內存,就是 JVM之外的機器內存,好比,你有4G的內存,JVM佔用了1G,則其他的3G就是直接內存,JDK中有一種基於通道(Channel)和緩衝區 (Buffer)的內存分配方式,將由C語言實現的native函數庫分配在直接內存中,用存儲在JVM堆中的DirectByteBuffer來引用。 因爲直接內存收到本機器內存的限制,因此也可能出現OutOfMemoryError的異常。

Java對象的訪問方式

通常來講,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。

  以最簡單的本地變量引用:Object obj = new Object()爲例:

  • Object obj表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;
  • new Object()做爲實例對象數據存儲在堆中;
  • 堆中還記錄了Object類的類型信息(接口、方法、field、對象類型等)的地址,這些地址所執行的數據存儲在方法區中;

在Java虛擬機規範中,對於經過reference類型引用訪問具體對象的方式並未作規定,目前主流的實現方式主要有兩種:

1,經過句柄訪問(圖來自於《深刻理解Java虛擬機:JVM高級特效與最佳實現》):

經過句柄訪問的實現方式中,JVM堆中會專門有一塊區域用來做爲句柄池,存儲相關句柄所執行的實例數據地址(包括在堆中地址和在方法區中的地址)。這種實現方法因爲用句柄表示地址,所以十分穩定。

2,經過直接指針訪問:(圖來自於《深刻理解Java虛擬機:JVM高級特效與最佳實現》)

經過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。這種方法最大的優點是速度快,在HotSpot虛擬機中用的就是這種方式。

Java內存分配機制

這裏所說的內存分配,主要指的是在堆上的分配,通常的,對象的內存分配都是在堆上進行,但現代技術也支持將對象拆成標量類型(標量類型即原子類型,表示單個值,能夠是基本類型或String等),而後在棧上分配,在棧上分配的不多見,咱們這裏不考慮。

  Java內存分配和回收的機制歸納的說,就是:分代分配,分代回收。對象將根據存活的時間被分爲:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。以下圖(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  年輕代(Young Generation):對象被建立時,內存的分配首先發生在年輕代(大對象能夠直接 被建立在年老代),大部分的對象在建立後很快就再也不使用,所以很快變得不可達,因而被年輕代的GC機制清理掉(IBM的研究代表,98%的對象都是很快消 亡的),這個GC機制被稱爲Minor GC或叫Young GC。注意,Minor GC並不表明年輕代內存不足,它事實上只表示在Eden區上的GC。

  年輕代上的內存分配是這樣的,年輕代能夠分爲3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示內存首次分配的區域,再 貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。內存分配過程爲(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  1. 絕大多數剛建立的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,所以在其上分配內存極快;
  2. 當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
  3. 此後,每次Eden區滿了,就執行一次Minor GC,並將剩餘的對象都添加到Survivor0;
  4. 當Survivor0也滿的時候,將其中仍然活着的對象直接複製到Survivor1,之後Eden區執行Minor GC後,就將剩餘的對象添加Survivor1(此時,Survivor0是空白的)。
  5. 當兩個存活區切換了幾回(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代)以後,仍然存活的對象(其實只有一小部分,好比,咱們本身定義的對象),將被複制到老年代。

  從上面的過程能夠看出,Eden區是連續的空間,且Survivor總有一個爲空。通過一次GC和複製,一個Survivor中保存着當前還活 着的對象,而Eden區和另外一個Survivor區的內容都再也不須要了,能夠直接清空,到下一次GC時,兩個Survivor的角色再互換。所以,這種方 式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的「中止-複製(Stop-and-copy)」清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另外一個Survivor中),這不表明着中止複製清理法很高效,其實,它也只在這種狀況下高效,若是在老年代採用中止複製,則挺悲劇的。

  在Eden區,HotSpot虛擬機使用了兩種技術來加快內存分配。分別是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),這兩種技術的作法分別是:因爲Eden區是連續的,所以bump-the-pointer技術的核心就是跟蹤最後建立的一個對象,在對 象建立時,只須要檢查最後一個對象後面是否有足夠的內存便可,從而大大加快內存分配速度;而對於TLAB技術是對於多線程而言的,將Eden區分爲若干 段,每一個線程使用獨立的一段,避免相互影響。TLAB結合bump-the-pointer技術,將保證每一個線程都使用Eden區的一段,並快速的分配內 存。

  年老代(Old Generation):對象若是在年輕代存活了足夠長的時間而沒有被清理掉(即在幾回 Young GC後存活了下來),則會被複制到年老代,年老代的空間通常比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時, 將執行Major GC,也叫 Full GC。  

   可使用-XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,若是動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。

  若是對象比較大(好比長字符串或大數組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發提早GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。

  可能存在年老代對象引用新生代對象的狀況,若是須要執行Young GC,則可能須要查詢整個老年代以肯定是否能夠清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——」card table「,全部老年代對象引用新生代對象的記錄都記錄在這裏。Young GC時,只要查這裏便可,不用再去查所有老年代,所以性能大大提升。

Java GC機制

GC機制的基本算法是:分代收集,這個不用贅述。下面闡述每一個分代的收集方法。

  

  年輕代:

  事實上,在上一節,已經介紹了新生代的主要垃圾回收方法,在新生代中,使用「中止-複製」算法進行清理,將新生代內存分爲2部分,1部分 Eden區較大,1部分Survivor比較小,並被劃分爲兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的對象拷貝到 另外一個Survivor中,而後清理掉Eden和剛纔的Survivor。

  這裏也能夠發現,中止複製算法中,用來複制的兩部分並不老是相等的(傳統的中止複製算法兩部份內存相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)

  因爲絕大部分的對象都是短命的,甚至存活不到Survivor中,因此,Eden區與Survivor的比例較大,HotSpot默認是 8:1,即分別佔新生代的80%,10%,10%。若是一次回收中,Survivor+Eden中存活下來的內存超過了10%,則須要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數來配置Eden區域Survivor區的容量比值,默認是8,表明Eden:Survivor1:Survivor2=8:1:1.

  老年代:

  老年代存儲的對象比年輕代多得多,並且不乏大對象,對老年代進行內存清理時,若是使用中止-複製算法,則至關低效。通常,老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將全部存活的對象向一端移動,以保證內存的連續。
     在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,若是大於,則直接觸發一次Full GC,不然,就查看是否設 置了-XX:+HandlePromotionFailure(容許擔保失敗),若是容許,則只會進行MinorGC,此時能夠容忍內存分配失敗;若是不 容許,則仍然進行Full GC(這表明着若是設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有不少內存,因此,最好不要這樣作)。

  方法區(永久代):

  永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就能夠被回收。對於無用的類進行回收,必須保證3點:

  1. 類的全部實例都已經被回收
  2. 加載類的ClassLoader已經被回收
  3. 類對象的Class對象沒有被引用(即沒有經過反射引用該類的地方)
     永久代的回收並非必須的,能夠經過參數來設置是否對類進行回收。HotSpot提供-Xnoclassgc進行控制
     使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading能夠查看類加載和卸載信息
     -verbose、-XX:+TraceClassLoading能夠在Product版HotSpot中使用;
     -XX:+TraceClassUnLoading須要fastdebug版HotSpot支持

垃圾收集器

在GC機制中,起重要做用的是垃圾收集器,垃圾收集器是GC的具體實現,Java虛擬機規範中對於垃圾收集器沒有任何規定,因此不一樣廠商實現的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器以下圖(圖來源於《深刻理解Java虛擬機:JVM高級特效與最佳實現》,圖中兩個收集器之間有連線,說明它們能夠配合使用):

  

  

在介紹垃圾收集器以前,須要明確一點,就是在新生代採用的中止複製算法中,「停 止(Stop-the-world)」的意義是在回收內存時,須要暫停其餘所 有線程的執行。這個是很低效的,如今的各類新生代收集器愈來愈優化這一點,但仍然只是將中止的時間變短,並未完全取消中止。

  • Serial收集器:新生代收集器,使用中止複製算法,使用一個線程進行GC,其它工做線程暫停。使用-XX:+UseSerialGC可使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)
  • ParNew收集器:新生代收集器,使用中止複製算法,Serial收集器的多線程版,用多個線程進行GC,其它工做線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。
  • Parallel Scavenge 收集器:新生代收集器,使用中止複製算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,好比:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行後臺運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間不多,因此適 合用戶交互,提升用戶體驗)。使用-XX:+UseParallelGC開關控制使用 Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間佔總時間的比例,默認99,即 1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效)
  • Serial Old收集器:老年代收集器,單線程收集器,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象幹掉,只留倖存 的對象,壓縮是將移動對象,將空間填滿保證內存分爲2塊,一塊全是對象,一塊空閒)算法,使用單線程進行GC,其它工做線程暫停(注意,在老年代中進行標 記整理算法清理,也須要暫停其它線程),在JDK1.5以前,Serial Old收集器與ParallelScavenge搭配使用。
  • Parallel Old收集器:老年代收集器,多線程,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不一樣,這裏的整理是Summary(彙總)和Compact(壓縮),彙總的意思就是將倖存的對象複製到預先準備好的區域,而不是像Sweep(清 理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然須要暫停其它線程。Parallel Old在多核計算中頗有用。Parallel Old出現後(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
  • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力於獲取最短回收停頓時間,使用標記清除算法,多線程,優勢是併發收集(用戶線程能夠和GC線程同時工做),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS(緣由見後面),當用戶線程內存不足時,採用備用方案Serial Old收集。
CMS收集的方法是:先3次標記,再1次清除,3次標記中前兩次是初始標記和從新標記(此時仍然須要中止(stop the world)), 初始標記(Initial Remark)是標記GC Roots能關聯到的對象(即有引用的對象),停頓時間很短;併發標記(Concurrent remark)是執行GC Roots查找引用的過程,不須要用戶線程停頓;從新標記(Remark)是在初始標記和併發標記期間,有標記變更的那部分仍須要標記,因此加上這一部分 標記的過程,停頓時間比並發標記小得多,但比初始標記稍長。在完成標記以後,就開始併發清除,不須要用戶線程停頓。
因此在CMS清理過程當中,只有初始標記和從新標記須要短暫停頓,併發標記和併發清除都不須要暫停用戶線程,所以效率很高,很適合高交互的場合。
CMS也有缺點,它須要消耗額外的CPU和內存資源,在CPU和內存資源緊張,CPU較少時,會加劇系統負擔(CMS默認啓動線程數爲(CPU數量+3)/4)。
另外,在併發收集過程當中,用戶線程仍然在運行,仍然產生內存垃圾,因此可能產生「浮動垃圾」,本次沒法清理,只能下一次Full GC才清理,所以在GC期間,須要預留足夠的內存給用戶線程使用。因此使用CMS的收集器並非老年代滿了才觸發Full GC,而是在使用了一大半(默認68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設置)的時候就要進行Full GC,若是用戶線程消耗內存不是特別大,能夠適當調高-XX:CMSInitiatingOccupancyFraction以下降GC次數,提升性能,若是預留的用戶線程內存不夠,則會觸發Concurrent Mode Failure,此時,將觸發備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了,所以-XX:CMSInitiatingOccupancyFraction不宜設的過大。
還有,CMS採用的是標記清除算法,會致使內存碎片的產生,可使用-XX:+UseCMSCompactAtFullCollection來設置是否在Full GC以後進行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設置在執行多少次不壓縮的Full GC以後,來一次帶壓縮的Full GC。
 
  • G1收集器:在JDK1.7中正式發佈,與現狀的新生代、老年代概念有很大不一樣,目前使用較少,不作介紹。
 
     注意併發(Concurrent)和並行(Parallel)的區別:
     併發是指用戶線程與GC線程同時執行(不必定是並行,可能交替,但整體上是在同時執行的),不須要停頓用戶線程(其實在CMS中用戶線程仍是須要停頓的,只是很是短,GC線程在另外一個CPU上執行);
     並行收集是指多個GC線程並行工做,但此時用戶線程是暫停的;
因此,Serial和Parallel收集器都是並行的,而CMS收集器是併發的.
 
關於JVM參數配置和內存調優實例,見個人下一篇博客(編寫中:Java系列筆記(4) - JVM監控與調優),原本想寫在同一篇博客裏的,無奈內容太多,只好另起一篇。
 
說明:
  本文是Java系列筆記的第3篇,這篇文章寫了好久,主要是Java內存和 GC機制相對複雜,難以理解,加上本人這段時間項目和生活中耗費的時間不少,因此進度緩慢。文中大多數筆記內容來源於我在網絡上查到的博客和《深刻理解 Java虛擬機:JVM高級特效與最佳實現》一書。
  本人能力有限,若是有錯漏,請留言指正。
參考資料:
《JAVA編程思想》,第5章;
《Java深度歷險》,Java垃圾回收機制與引用類型;
《深刻理解Java虛擬機:JVM高級特效與最佳實現》,第2-3章;
成爲JavaGC專家Part II — 如何監控Java垃圾回收機制, http://www.importnew.com/2057.html
JDK5.0垃圾收集優化之--Don't Pause,http://calvin.iteye.com/blog/91905
【原】java內存區域理解-初步瞭解,http://iamzhongyong.iteye.com/blog/1333100
相關文章
相關標籤/搜索