在介紹GC之前有必要先了解1下JVM的內存劃分,這樣在后面介紹GC和各種不同的GC collector的時候更容易理解。
下面這張圖是“偷”的他人的,很經典的描寫了jvm的體系結構,我們只需要關注最大的那1塊――運行時數據區域。
運行時區顧名思義是jvm在運行時的內存結構,主要有以下5種。
1.方法區
方法區是各個線程同享的1塊內存區域,當虛擬機裝載1個class文件時,它會從2進制數據中解析類型的信息,這些信息便是存儲在方法區,包括類的靜態變量也會存儲到該區域。虛擬機規范把該區域劃分為堆的1部份,但是實際上它還有個別名Non-Heap,很明顯是用來和堆做辨別的。在討論GC時我們習慣把這個區域叫做永久代,本質上它倆不是1個概念,對HotSpot來講,永久代僅僅是實現方法區的1種方式。并且HotSpot后續的版本計劃移除永久代,如果該區域內存不足時,會產生OOM。
2.堆
堆和方法區1樣也是各個線程同享的1塊內存區域,所有的對象實例包括數組都在這里分配內存。比如當我們new Object()的時候便是在這里分配的內存,java堆可以說是jvm管理的最大的1塊內存區域,需要注意的1點是和方法區1樣jvm規范并沒有要求堆是連續的,jvm可以在運行時動態的擴大和收縮堆。為了更好的實現GC,現代的jvm針對堆又做了細化,將1整塊堆分成不同的區域。下面這張圖來自oracle官方網站,詳細的畫出了堆的詳細情況。
圖倒是蠻大的- -,還是橫著的,湊合著看吧,全部堆分為3個區域,Young區、Tenured區(也就是Old區)、Perm區,習慣上我們稱為年輕代,年老代,永久代(實際上GC就是依照這3個代進行分代搜集的)。仔細的朋友可能會注意到每一個區域都有1塊virtual,有必要說明1下virtual是做甚么,我們知道堆可以在運行時擴充,比如在配置虛擬機參數的時候通常會指定-Xmx,-Xms最大堆和初始堆,這里的virtual就是預留的內存區域,其值為最大堆減去初始堆的值,實際上操作系統1開始就會劃分-Xmx大小的內存給jvm,只不過jvm1開始可能不需要那末大的空間,因此jvm將1部份內存標記為virtual區,留著后面擴大用。3種內存區域中Young區略微復雜些,這里又分為3個區域分別為1個Eden和兩個Survivor區(1個to survivor1個from survivor),名字很成心思,1個是伊甸園1個是幸存區,關于這幾個區具體寄存的是甚么后面做進1步解釋。
3.虛擬機棧
虛擬機棧為線程私有,因此處于這個區域的值不需要斟酌并發的問題。jvm為每一個java線程都分配1個棧,為該線程私有,棧內存隨著線程的燒毀而釋放。jvm為每一個方法都會生成1個棧幀用于保存局部變量表,操作數棧(這些概念在我之前的博客中也提到過)等信息。基本類型和對象的援用都可以在棧中存儲。該區域有可能拋出StackOverflowerror和OOM異常。
4.本地方法棧
本地方法棧類似虛擬機棧,只不過虛擬機棧是為java方法服務,本地方法棧為本地的Native方法服務。
5.程序計數器
程序計數器是1塊非常小的內存區域,它是當前線程履行的字節碼的行號唆使器,總是指向下1個要履行的指令,該區域是唯1不會產生OOM的內存區。
上面說過虛擬機棧,本地方法棧和程序計數器它們的內存分配在編譯期基本就能夠肯定,并且內存隨著線程的方法結束或線程結束而燒毀,因此這部份內存不需要斟酌回收的問題。堆和方法區的內存分配相比來講就具有了不肯定性,而且這部份的內存分配和回收都是動態的,因此jvm需要針對這兩塊內存做GC。
趁熱打鐵,剛剛講完堆的分代,正好來看下為何要分代和各個代中存儲的對象有何不同。
之所以要分代很明顯的1個緣由是方便做垃圾搜集,由于垃圾搜集只是針對那些沒有被援用的孤對象進行的,而研究表明java中大多數的對象都是短命的,但也有1些對象存活的時間比較長。因此為了針對這些生命不1的對象做搜集,將堆劃分為不同的代來寄存這些對象,也就是說Young區中的對象都是比較“年輕的”,同理可理解Old區。這就是為何叫做Young和Old的緣由。
實際上GC其實不是java獨有的,GC的歷史要比java悠久。關于GC的基本原理比如援用計數法,可達性分析法等等就不作介紹了。GC主要有兩種,1種是minor gc另外一種是major gc也有稱為young gc和old gc的。minor gc產生在Young區,并且時間通常非常短,major gc產生在Old區,時間較長,需要控制major gc的次數和GC時間。
jvm依照對象存活的時間,給對象1個類似我們人類“年齡的概念”,實際上每經過1次GC,存活下來的對象年齡便+1,大多數情況下對象優先分配到Eden區,這就是為何這里叫Eden區的緣由,當Eden區的沒有足夠內存分配的時候,便會觸發1次Minor GC。此時存活下來的對象年齡+1,當到達1定年齡的時候,表明該對象存活比較穩定,會把該部份對象移到年老代(我們可以通過參數-XX:MaxTenuringThreshold 控制經過量少次Minor gc后便進入old區,默許為15),如果在Minor GC的時候發現to survivor寄存不下這些對象,則會直接寄存到年老代。需要注意的1點是當要分配大對象(比如數組和長字符串)的時候,也是直接將他們分配到年老代的,因此我們盡可能避免大對象特別是短命大對象的使用,由于這很容易引發old區的內存不夠分配,從而提早觸發full gc。當年老代沒有足夠內存的時候便會觸發Major GC,通常minor gc的時間非常短對程序影響可以疏忽,但是major gc的進程則要比minor gc長很多,因此要盡可能避免major gc的產生(當產生gc的時候會暫停所有的利用線程履行,官方稱為stop the world,這里的時間指的就是stop the world的時間)。
來看1下目前幾種主要的GC算法,之所以這些算法并存是由于針對不同的區域通常需要使用特定的算法。
1.標記-清除
很明顯該算法分為兩步,第1步是標記出需要回收的對象,第2步針對這些對象做清算動作。該算法的缺點主要有兩個:1是效力問題,標記和清除的效力都不高,2是清除以后會產生大量不連續的內存碎片,這會致使運行進程中如果需要分配較大的對象,會由于找不到足夠的連續內存而提早觸發1次垃圾搜集。
2.復制算法
復制算法將內存分為大小相等的兩塊,每次使用1塊,當1塊使用完時,將還存活的對象全部復制到另外一塊區域中,然后清算第1塊的內存,該算法的好處不言而喻,不會產生內存碎片,但是只使用1塊內存未免代價太大,并且在存活對象非常多的時候,效力肯定會低下。實際上在java中絕大多數的對象都是“短命的”,因此不需要依照1:1來劃份內存,HotSpot默許將Eden區和兩個survivor區依照8:2的比例進行劃分,也就是Eden:survivor=8:1(固然我們可以通過-XX:SurvivorRatio設置Survivor的大小,該值如果為8,則表示Eden區占Young的10分之8,兩個Survivor占Young區的10分之2),該算法非常合適在minor gc時使用。
3.標記-整理
復制算法針對有大量存活的對象時效力低下,因此不合適對年老代的回收,但是標記清除又有碎片的問題,因此產生了標記整理算法。標記整理算法和標記清除算法類似,第1步都是標記,而第2步整理階段是將存活的對象移向1端,然后直接清算邊界之外的內存,這樣不會產生碎片效力也不算太差。
針對上面幾種算法,HotSpot主要提供了以下幾種實現:
圖中黃色背景是年輕代的搜集器,淺灰色背景是年老代的搜集器,藍色背景表示垃圾搜集器,兩兩直線相連表示兩種搜集器可以共用。下面分別介紹1下這6種搜集器:
1."Serial"會引發stop the world,基于拷貝的單線程搜集器。
2."ParNew"即是serial的多線程版本,不同于 "Parallel Scavenge" ,ParNew可以和CMS配合1起使用威力更大。
3. "Parallel Scavenge"會引發stop the world,基于拷貝的多線程搜集器。
4."Serial Old"會引發stop the world,基于標記-清除-整理的單線程搜集器。
5."CMS"是1種并發短暫停的搜集器,其中的某些步會引發stw,后面詳細講授。
6."Parallel Old"1種并發的基于標記-整理的搜集器,Parallel Scavenge的年老代版本。
以上6種最復雜的是CMS搜集器,后面會詳細講授。
ParNew是多線程并行搜集器,CMS是并發搜集。這里不能不提1句,并行指的是多個垃圾搜集線程1起進行回收工作,此時利用線程是停止,但是并發指的是搜集線程和利用線程同時履行,也就是垃圾搜集工作的時候其實不影響利用(這里的不影響只是相對的,實際CMS的工作進程分為好多步,有些步驟也會產生stop the world)。
既然ParNew和Parallel Scavenge都是針對新生代的并行搜集,那末他們兩個有甚么不同呢?
像CMS和ParNew等搜集器主要關注的是減少因搜集而引發的利用停頓時間,而Parallel Scavenge主要關注的是利用的吞吐量,所謂的吞吐量就是CPU用于運行利用程序時間和CPU總消耗時間的比值,比如虛擬機總共運行100分鐘,而垃圾搜集用掉了1分鐘,吞吐量即為99/100=99%。而對Parallel Scavenge有個特殊的參數 -XX:+UseAdaptiveSizePolicy利用該參數JVM可以在運行時自動調劑堆內存各個區的大小,不需要人為的配置。
針對虛擬機參數使用-XX配置不同的搜集器,主要有以下幾種:
UseSerialGC 是"Serial" + "Serial Old"
UseParNewGC 是 "ParNew" + "Serial Old"
UseConcMarkSweepGC 是"ParNew" + "CMS" + "Serial Old"。 年老代的回收絕大多數時間使用"CMS"。但是當產生concurrent mode failure毛病的時候會切換到"Serial Old" 。
UseParallelGC是"Parallel Scavenge" + "Serial Old"
UseParallelOldGC是"Parallel Scavenge" + "Parallel Old"
上面這張圖是CMS搜集器的幾個工作階段分別是:初始標記,并發標記,重新標記,并發清除。其中的1,3兩個步驟需要暫停所有的利用程序線程的。第1次暫停從root對象開始標記存活的對象,這個階段稱為初始標記;第2次暫停是在并發標記以后, 暫停所有利用程序線程,重新標記并發標記階段遺漏的對象(在并發標記階段結束后對象狀態的更新致使)。第1次暫停會比較短,第2次暫停通常會比較長,并且 remark這個階段可以并行標記。1個CMS會產生兩次STW。因此在使用CMS的垃圾搜集器的時候,通常我們使用jstat查看的fullgc(有1種說法是fullgc的次數就是STW的次數)次數和cms產生的次數為2:1的關系。關于CMS的參數有很多需要關注的點:
其他的1些關于GC的參數還有下面1些:
-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細日志
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的情勢)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的情勢,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸前途徑
上面個的參數主要觸及GC日志的打印,jvm還有很多其他的參數不逐一描寫了,網上有很多詳細的講授。
講了那末多GC,下面來分析1段GC日志
519.514: [GC 519.514: [ParNew: 5149852K->83183K(5662336K), 0.0831770 secs] 6955196K->1905793K(9856640K), 0.0833560 secs] [Times: user=0.57 sys=0.03, real=0.08 secs ]
前面的519.514表示了自虛擬機啟動到該GC產生的秒數,[GC表示本次是普通的GC固然還有[Full GC,[ParNew表示使用的是ParNew搜集器對年輕代做搜集, 5149852K->83183K(5662336K)分別表示GC前該區域已使用的內存大小,GC后該區域使用的內存大小,該區域的總大小。 0.0831770 secs表示GC所占用的時間單位為秒,后面更詳細的時間user=0.57 sys=0.03, real=0.08 secs與Linux的time命令所輸出的時間含義1致。