復制於 http://www.cnblogs.com/fefjay/p/6297340.html
當JVM創建對象遇到內存不足的時候,JVM會自動觸發垃圾回收garbage collecting(簡稱GC)操作,將不再使用但仍存在JVM內存中的對象當做垃圾一樣直接清理掉,釋放被占用的內存空間,供新創建的對象使用。
那么問題來了,要讓系統能夠自動實現不被引用對象的回收,有幾個問題需要解決:
- Who:哪些是不再使用要被當做“垃圾”回收處理的對象?也就是要確定垃圾對象。
- Where:在哪里執行垃圾回收?明確要清理的內存區域。
- When:什么時候執行GC操作?即JVM觸發GC的時機。
- How:怎么樣進行垃圾對象處理?即GC的實現算法。
關於第二點,對於oracle Hotspot VM的 GC操作主要回收的內存區域是JVM中的堆(分為年輕代和年老代,年輕代又分Eden和兩個survivor區域),JVM內存結構不是本文的重點,我們在看本文前要對JVM內存結構有一定的了解,這里不作詳細分析。
1.判斷對象是否可以被回收(判別算法或搜索算法)
下面的算法回答了who的問題。
1.1 引用計數法
每個對象創建的時候,會分配一個引用計數器,當這個對象被引用的時候計數器就加1,當不被引用或者引用失效的時候計數器就會減1。任何時候,對象的引用計數器值為0就說明這個對象不被使用了,就認為是“垃圾”,可以被GC處理掉。
評價:
- 【優點】算法實現簡單。
- 【缺點】不能解決對象之間循環引用的問題。有垃圾對象不能被正確識別,這對垃圾回收來說是很致命的,所以GC並沒有使用這種搜索算法。
1.2 根搜索算法
以一些特定的對象作為基礎原始對象,或者稱作“根”,不斷往下搜索,到達某一個對象的路徑稱為引用鏈。
如果一個對象和根對象之間有引用鏈,即根對象到這個對象是可到達的,則這個對象是活着的,不是垃圾,還不能回收。例如,假設有根對象O,O引用了A對象,同時A對象引用了B對象,B對象又引用了C對象,那么對象C和根對象O之間的路徑的可達的,C對象就不能當做垃圾對象。引用鏈為O->A->B->C。
反之,如果一個對象和根對象之間沒有引用鏈,根對象到這個對象的路徑是不可達的,那么這個對象就是可回收的垃圾對象。
評價:
- 【優點】可找到所以得垃圾對象,並且完美解決對象之間循環引用的問題。
- 【缺點】不可避免地要遍歷全局所有對象,導致搜索效率不高。
根搜索算法是現在GC使用的搜索算法。
可以當做GC roots的對象有以下幾種:
- 虛擬機棧中的引用的對象。(java棧的棧幀本地變量表)
- 方法區中的類靜態屬性引用的對象。
- 方法區中的常量引用的對象。(聲明為final的常量對象)
-
本地方法棧中JNI的引用的對象。(本地方法棧的棧幀本地變量表)
下面是從網上找來的圖,將就看看:GC ROOTS就是跟對象節點,藍色的是可達的引用鏈,引用鏈上的對象是活着的,不能被當做垃圾對象回收。相反暗灰色的路徑表示不可達的路徑,這些對象將會被回收。每個圈圈里面的數字,表示其被引用的次數,沒錯,就是上面說到的引用計數法的計數值。
2.GC算法
這里討論的是oracle的Hotspot VM常見的垃圾回收算法。使用的搜索算法都是基於根搜索算法實現的。
2.1 標記-清除算法(Mark-Sweep)
該算法分兩步執行:
1) 標記Mark:從GC ROOTS開始,遍歷堆內存區域的所有根對象,對在引用鏈上的對象都進行標記。這樣下來,如果是存活的對象就會被做了標記,反之如果是垃圾對象,則沒做有標記。GC很容易根據有沒有被做標記就完成了垃圾對象回收。
2) 清除Sweep:遍歷堆中的所有的對象(標記階段遍歷的是所有根節點),找到未被標記的對象,直接回收所占的內存,釋放空間。
評價:
- 【優點】沒有產生額外的內存空間消耗,內存利用率高。
- 【缺點】效率低,清除階段要遍歷所有的對象;回收的垃圾對象是在各個角落的,直接回收垃圾對象,導致存在不連續的內存空間,產生內存碎片。
標記-清除算法操作的對象是【垃圾對象】,對於活着的對象(被標記的對象),它則直接不理睬。
2.2 復制算法(Copying)
復制算法把內存區間一分為二,有對象存在的一半區間稱為“活動區間”,沒有對象存在處於空閑狀態的空間則為“空閑區間”。
當內存空間不足時觸發GC,先采用根搜索算法標記對象,然后把活着的對象全部復制到另一半空閑區間上,復制算法的“復制”就來自這一操作。復制到另一半區間的時候,嚴格按照內存地址依次排列要存放的對象,然后一次性回收垃圾對象。
這樣原來的空閑區間在GC后就變成活動區間,而且內存順序齊整美觀。原來的活動區間在GC后就變成了完全空的空閑區間,等待下一次GC把活的對象被copy進來。
評價:
- 【優點】GC后的內存齊整,不產生內存碎片。
- 【缺點】GC要使用兩倍的內存,或者說導致堆只能使用被分配到的內存的一半,這個算法對空間要求太高!如果存活的對象較多,則意味着要復制很多對象並且要維護大量對象的內存地址,所以存活的對象數量不能太多,否則效率也會很低。
復制算法復制移動的對象是【活着的對象】,對於垃圾對象(不被標記的對象)則直接回收。
2.3 標記-整理算法(Mark-Compact)
這個算法則是對上面兩個算法的綜合結果。也分為兩個階段:
1)標記:這個階段和標記-清除Mark-Sweep算法一樣,遍歷GC ROOTS並標記存活的對象。
2)整理:移動所有活着的對象到內存區域的一側(具體在哪一側則由GC實現),嚴格按照內存地址次序依次排列活着的對象,然后將最后一個活着的對象地址以后的空間全部回收。
評價:
-
【優點】內存空間利用率高,消除了復制算法內存減半的情況;GC后不會產生內存碎片。
-
【缺點】需要遍歷標記活着的對象,效率較低;復制移動對象后,還要維護這些活着對象的引用地址列表。
2.4 分代回收算法(Generational Collecting)
分代回收算法就是現在JVM使用的GC回收算法。
2.4.1簡要說明
1)先來看看簡單化后的堆的內存結構:
Java堆 = 年老代 + 年輕代
(空間大小比例一般是3:1) 年輕代 = Eden區 + From Space區 + To Space區 (空間大小比例一般是8:1:1)
2)按照對象存活時間長短,我們可以把對象簡單分為三類:
-
短命對象:存活時間較短的對象,如中間變量對象、臨時對象、循環體創建的對象等。這也是產生最多數量的對象,GC回收的關注重點。
-
長命對象:存活時間較長的對象,如單例模式產生的單例對象、數據庫連接對象、緩存對象等。
-
長生對象:一旦創建則一直存活,幾乎不死的對象。
3)對象分配區域
短命對象存在於年輕代,長命對象存在於年老代,而長生對象則存在於方法區中。
由於GC的主要內存區域是堆,所以GC的對象主要就是短命對象和長命對象這類壽命“有限”的對象。
2.4.2 分代回收的GC類型
針對HotSpot VM的的GC其實准確分類只有兩大種:
1)Partial GC:部分回收模式
- Young GC:只收集young gen的GC。和Minor GC一樣。
- Old GC:只收集old gen的GC。只有CMS的concurrent - collection是這個模式
- Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
2)Full GC:收集整個堆,包括young gen、old gen,還有永久代perm gen(如果存在的話)等所有部分的模式。同Major GC。
3)觸發時機
HotSpot VM的串行GC的觸發條件是:
young GC:當young gen中的eden區分配滿的時候觸發。
full GC:當准備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發young GC而是轉為觸發full GC;或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。
並發GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做並發收集。
2.4.3 年輕代GC過程
當需要在堆中創建一個新的對象,而年輕代內存不足時觸發一次GC,在年輕代觸發的GC稱為普通GC,Minor GC。注意到年輕代中的對象都是存活時間較短的對象,所以適合使用復制算法。這里肯定不會使用兩倍的內存來實現復制算法了,牛人們是這樣解決的,把年輕代內存組成是80%的Eden、10%的From Space和10%的To Space,然后在這些內存區域直接進行復制。
剛開始創建的對象是在Eden中,此時Eden中有對象,而兩個survivor區沒有對象,都是空閑區間。第一次Minor GC后,存活的對象被放到其中一個survivor,Eden中的內存空間直接被回收。在下一次GC到來時,Eden和一個survivor中又創建滿了對象,這個時候GC清除的就是Eden和這個放滿對象的survivor組成的大區域(占90%),Minor GC使用復制算法把活的對象復制到另一個空閑的survivor區間,然后直接回收之前90%的內存。周而復始。始終會有一個10%空閑的survivor區間,作為下一次Minor GC存放對象的准備空間。
要完成上面的算法,每次Minor GC過程都要滿足:
存活的對象大小都不能超過survivor那10%的內存空間,不然就沒有空間復制剩下的對象了。但是,萬一超過了呢?前面我們提到過年老代,對,就是把這些大對象放到年老代。
2.4.4 年老代GC
什么樣的對象可以進入年老代呢?如下:
- 在年輕代中,如果一個對象的年齡(GC一次后還存活的對象年歲加1)達到一個閾值(可以配置),就會被移動到年老代。
- Survivor中相同年齡的對象大小總和超過survivor空間的一半,則不小於這個年齡的對象都會直接進入年老代。
- 創建的對象的大小超過設定閾值,這個對象會被直接存進年老代。
- 年輕代中大於survivor空間的對象,Minor GC時會被移進年老代。
年老代中的對象特點就是存活時間較長,而且沒有備用的空閑空間,所以顯然不適合使用復制算法了,這個時候使用標記-清除算法或者標記-整理算法來實現GC。負責年老代中GC操作的是全局GC,Major GC,Full GC。
什么時候觸發Major GC呢?
在Minor GC時,先檢測JVM的統計數據,查看歷史上進入老年代的對象平均大小是否大於目前年老代中的剩余空間,如果大於則觸發Full GC。
3.GC執行機制
3.1串行GC
在搜索掃描和復制過程都是采用單線程實現,適用於單CPU、新生代空間較小或者要求GC暫停時間要求不高的地方。是client級別的默認方式。
3.2並行GC
在搜索掃描和復制過程都是采用多線程實現,適用於多CPU、或者要求GC暫停時間要求高的地方。是server級別的默認方式。
3.3同步GC
同時允許多個GC任務,減少GC暫停時間。主要應用在實時性要求重於總體吞吐量要求的中大型應用,即使如此,降低中斷時間的技術還是會導致應用程序性能的降低。
4.內存調優
JVM內存調優,主要是減少GC的頻率和減少Full GC的次數,Full GC的時候會極大地影響系統的性能。所以在此基礎上,更加要關注會導致Full GC的情況。
4.1 容易導致Full GC的情況
-
年老代空間不足
1)分配足夠大空間給old gen。
2)避免直接創建過大對象或者數組,否則會繞過年輕代直接進入年老代。
3)應該使對象盡量在年輕代就被回收,或待得時間盡量久,避免過早的把對象移進年老代。 -
方法區的永久代空間不足
1)分配足夠大空間給。
2)避免創建過多的靜態對象。 -
被顯示調用System.gc()
通常情況下不要顯示地觸發GC,讓JVM根據自己的機制實現。
4.2 JVM堆內存分配問題討論
4.2.1 年輕代過小(年老代過大)
- 導致頻繁發生GC,增大系統消耗
- 容易讓普通大文件直接進入年老代,從而更容易誘發Full GC。
4.2.2 年輕代過大(年老大過小)
- 導致年老代過小,從而更容易誘發Full GC。
- GC耗時增加,降低GC的效率。
4.2.3 Eden過大(survivor過小)
Minor GC時容易讓普通大文件直接繞過survivor進入年老代,從而更容易誘發Full GC。
4.2.4Eden過小(survivor過大)
導致GC頻率升高,影響系統性能。
4.3 調優策略
- 保證系統吞吐量優先
- 減少GC暫停時間優先
5 JVM常見配置選項
總結一下常見配置。
堆設置
-Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:設置年輕代大小 -XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4 -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5 -XX:MaxPermSize=n:設置持久代大小 收集器設置 -XX:+UseSerialGC:設置串行收集器 -XX:+UseParallelGC:設置並行收集器 -XX:+UseParalledlOldGC:設置並行年老代收集器 -XX:+UseConcMarkSweepGC:設置並發收集器 垃圾回收統計信息 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename 並行收集器設置 -XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。 -XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間 -XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n) 並發收集器設置 -XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。 -XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。