Java與C++等語言最大的技術區別:自動化的垃圾回收機制(GC)
為什么要了解GC和內存分配策略
1、面試需要
2、GC對應用的性能是有影響的;
3、寫代碼有好處
棧:棧中的生命周期是跟隨線程,所以一般不需要關注
堆:堆中的對象是垃圾回收的重點
方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點
判斷對象的存活
引用計數法
給對象添加一個引用計數器,當對象增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的對象可被回收。(Python在用,但主流虛擬機沒有使用)
優點:快,方便,實現簡單。
缺陷:對象相互引用時(A.instance=B同時B.instance=A),很難判斷對象是否該回收。
可達性分析(Java中使用)
(面試時重要的知識點,牢記)
來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
作為GC Roots的對象包括下面幾種:
當前虛擬機棧中局部變量表中的引用的對象
當前本地方法棧中局部變量表中的引用的對象
方法區中類靜態屬性引用的對象
方法區中的常量引用的對象
請忘記 finalize
finalize可以完成對象的拯救,但是JVM不保證一定能執行,所以請忘記這個“坑”。
各種引用(Reference)
傳統定義:Reference中存儲的數據代表的是另一塊內存的起始地址。
強引用
一般的Object obj = new Object() ,就屬于強引用。
(如果有GCroots的強引用)垃圾回收器絕對不會回收它,當內存不足時寧愿拋出 OOM 錯誤,使得程序異常停止
軟引用 SoftReference
垃圾回收器在內存充足的時候不會回收它,而在內存不足時會回收它
軟引用非常適合于創建緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。
一些有用但是并非必需,用軟引用關聯的對象,系統將要發生OOM之前,這些對象就會被回收。參見代碼:
VM參數 -Xms10m ?-Xmx10m -XX:+PrintGC
?
運行結果
?
例如,一個程序用來處理用戶提供的圖片。如果將所有圖片讀入內存,這樣雖然可以很快的打開圖片,但內存空間使用巨大,一些使用較少的圖片浪費內存空間,需要手動從內存中移除。如果每次打開圖片都從磁盤文件中讀取到內存再顯示出來,雖然內存占用較少,但一些經常使用的圖片每次打開都要訪問磁盤,代價巨大。這個時候就可以用軟引用構建緩存。
弱引用 WeakReference
垃圾回收器在掃描到該對象時,無論內存充足與否,都會回收該對象的內存。
一些有用(程度比軟引用更低)但是并非必需,用弱引用關聯的對象,只能生存到下一次垃圾回收之前,GC發生時,不管內存夠不夠,都會被回收。
參看代碼:
?
?
注意:軟引用 SoftReference和弱引用 WeakReference,可以用在內存資源緊張的情況下以及創建不是很重要的數據緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。
實際運用(WeakHashMap、ThreadLocal)
虛引用 PhantomReference
幽靈引用,最弱,被垃圾回收的時候收到一個通知
如果一個對象只具有虛引用,那么它和沒有任何引用一樣,任何時候都可能被回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動
GC(Garbage Collection)
案例Oom類
-Xms ?堆區內存初始內存分配的大小
-Xmx ?堆區內存可被分配的最大上限
-XX:+PrintGCDetails
打印GC詳情
-XX:+HeapDumpOnOutOfMemoryError
當堆內存空間溢出時輸出堆的內存快照
新生代大小配置參數的優先級:
中間 -Xmn ?限定大小
?
-XX:SurvivorRatio
2個Survivor區和Eden區的比值
8 表示 兩個Survivor : Eden = 2: 8 ,每個Survivor占 1/10
可以修改為2
8 表示 兩個Survivor : Eden = 2: 2 ?,各占一半
GC overhead limit exceeded 超過98%的時間用來做GC并且回收了不到2%的堆內存時會拋出此異常
1.垃圾回收會占據資源
2.回收效率過低也會有限制
為什么new出的對象不會被回收了,我們來看看GC是如何判斷對象的存活
?
?
Minor GC
特點: 發生在新生代上,發生的較頻繁,執行速度較快
觸發條件: Eden區空間不足\空間分配擔保
Full GC
特點:主要發生在老年代上(新生代也會回收),較少發生,執行速度較慢
觸發條件:
調用 System.gc()
老年代區域空間不足
空間分配擔保失敗
JDK 1.7 及以前的永久代(方法區)空間不足
CMS GC處理浮動垃圾時,如果新生代空間不足,則采用空間分配擔保機制,如果老年代空間不足,則觸發Full GC
垃圾回收算法
復制算法(Copying)
將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半。
注意:內存移動是必須實打實的移動(復制),不能使用指針玩。
?
專門研究表明,新生代中的對象98%是“朝生夕死”的,所以一般來說回收占據10%的空間夠用了,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。
HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。
標記-清除算法(Mark-Sweep)
過程:
- 首先標記所有需要回收的對象
- 統一回收被標記的對象
缺點:
1.效率問題,標記和清除效率都不高
2.標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
標記-整理算法(Mark-Compact)
首先標記出所有需要回收的對象,在標記完成后,后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
垃圾回收器
分代收集
根據各個年代的特點選取不同的垃圾收集算法
新生代使用復制算法
老年代使用標記-整理或者標記-清除算法
jps -v顯示當前使用的垃圾回收器
?
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
請記住下圖的垃圾收集器和之間的連線關系。
?
并行:垃圾收集的多線程的同時進行。
并發:垃圾收集的多線程和應用的多線程同時進行。
?
注:吞吐量=運行用戶代碼時間/(運行用戶代碼時間+ 垃圾收集時間)
垃圾收集時間= 垃圾回收頻率 * 單次垃圾回收時間
各種垃圾回收器
Serial/Serial Old
最古老的,單線程,獨占式,成熟,適合單CPU ?服務器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old
?
ParNew
和Serial基本沒區別,唯一的區別:多線程,多CPU的,停頓時間比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合工作。
?
Parallel Scavenge(ParallerGC)/Parallel Old
關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。
所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
?
Concurrent Mark Sweep (CMS)
收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
-XX:+UseConcMarkSweepGC ,一般新生代使用ParNew,老年代的用CMS
從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標記—清除”算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,
垃圾回收過程
整個過程分為4個步驟,包括:
l?初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓(STW -Stop the world)。
l?并發標記:從GC Root 開始對堆中對象進行可達性分析,找到存活對象,它在整個回收過程中耗時最長,不需要停頓。
l?重新標記:為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓(STW)。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
l?并發清除:不需要停頓。
?
優點:
由于整個過程中耗時最長的并發標記和并發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。
缺點:
CPU資源敏感:因為并發階段多線程占據CPU資源,如果CPU資源不足,效率會明顯降低。
浮動垃圾:由于CMS并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
由于浮動垃圾的存在,因此需要預留出一部分內存,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。
在1.6的版本中老年代空間使用率閾值(92%)
如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啟用 Serial Old 來替代 CMS。
會產生空間碎片:標記 - 清除算法會導致產生不連續的空間碎片
?
G1垃圾回收器
?
G1中重要的參數:
-XX:+UseG1GC ??使用G1垃圾回收器
?
內部布局改變
G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再物理隔離。
算法:標記—整理 (humongous) 和復制回收算法(survivor)。
?
GC模式
Young GC
選定所有年輕代里的Region。通過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。(復制回收算法)
Mixed GC
選定所有年輕代里的Region,外加根據global concurrent marking統計得出收集收益高的若干老年代Region。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代Region。
Mixed GC不是full GC,它只能回收部分老年代的Region。如果mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GC(full GC)來收集整個GC heap。所以我們可以知道,G1是不提供full GC的。
全局并發標記(global concurrent marking)
?
初始標記:僅僅只是標記一下GC Roots 能直接關聯到的對象,并且修改TAMS(Nest Top Mark Start)的值,讓下一階段用戶程序并發運行時,能在正確可以的Region中創建對象,此階段需要停頓線程(STW),但耗時很短。
?
并發標記:從GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序并發執行。
?
最終標記:為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標記階段需要把 Remembered Set Logs 的數據合并到 Remembered Set 中。這階段需要停頓線程(STW),但是可并行執行。
?
篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起并發執行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
?
?
特點
空間整合:不會產生內存碎片
算法:標記—整理 (humongous) 和復制回收算法(survivor)。
可預測的停頓:
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
G1把內存“化整為零”的思路,理解起來似
G1 GC主要的參數
參數 | 含義 |
-XX:G1HeapRegionSize=n | 設置Region大小,并非最終值 |
-XX:MaxGCPauseMillis | 設置G1收集過程目標時間,默認值200ms,不是硬性條件 |
-XX:G1NewSizePercent | 新生代最小值,默認值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默認值60% |
-XX:ParallelGCThreads | STW期間,并行GC線程數 |
-XX:ConcGCThreads=n | 并發標記階段,并行執行的線程數 |
-XX:InitiatingHeapOccupancyPercent | 設置觸發標記周期的 Java 堆占用率閾值。默認值是45%。這里的java堆占比指的是non_young_capacity_bytes,包括old+humongous |
?
垃圾回收器的重要參數(使用-XX:)
參數 | 描述 |
UseSerialGC | 虛擬機運行在Client模式下的默認值,打開此開關后,使用 Serial+Serial Old 的收集器組合進行內存回收 |
UseParNewGC | 打開此開關后,使用 ParNew + Serial Old 的收集器組合進行內存回收 |
UseConcMarkSweepGC | 打開此開關后,使用 ParNew + CMS + Serial Old 的收集器組合進行內存回收。Serial Old 收集器將作為 CMS 收集器出現 Concurrent Mode Failure 失敗后的后備收集器使用 |
UseParallelGC | 虛擬機運行在 Server 模式下的默認值,打開此開關后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行內存回收 |
UseParallelOldGC | 打開此開關后,使用 Parallel Scavenge + Parallel Old 的收集器組合進行內存回收 |
SurvivorRatio | 新生代中 Eden 區域與 Survivor 區域的容量比值,默認為8,代表 Eden : Survivor = 8 : 1 |
PretenureSizeThreshold | 直接晉升到老年代的對象大小,設置這個參數后,大于這個參數的對象將直接在老年代分配 |
MaxTenuringThreshold | 晉升到老年代的對象年齡,每個對象在堅持過一次 Minor GC 之后,年齡就增加1,當超過這個參數值時就進入老年代 |
UseAdaptiveSizePolicy | 動態調整 Java 堆中各個區域的大小以及進入老年代的年齡 |
HandlePromotionFailure | 是否允許分配擔保失敗,即老年代的剩余空間不足以應付新生代的整個 Eden 和 Survivor 區的所有對象都存活的極端情況 |
ParallelGCThreads | 設置并行GC時進行內存回收的線程數 |
GCTimeRatio | GC 時間占總時間的比率,默認值為99,即允許 1% 的GC時間,僅在使用 Parallel Scavenge 收集器生效 |
MaxGCPauseMillis | 設置 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效 |
CMSInitiatingOccupancyFraction | 設置 CMS 收集器在老年代空間被使用多少后觸發垃圾收集,默認值為 68%,僅在使用 CMS 收集器時生效 |
UseCMSCompactAtFullCollection | 設置 CMS 收集器在完成垃圾收集后是否要進行一次內存碎片整理,僅在使用 CMS 收集器時生效 |
CMSFullGCsBeforeCompaction | 設置 CMS 收集器在進行若干次垃圾收集后再啟動一次內存碎片整理,僅在使用 CMS 收集器時生效 |
?
Stop The World現象
GC收集器和我們GC調優的目標就是盡可能的減少STW的時間和次數。