JVM-垃圾收集器与内存分配策略
對象存活判定方法
引用計數算法
對象引用計算器,引用加1,失效減1。計數為0表示對象死亡。
JVM不采用,因為互相引用導致循環引用問題
可達性分析算法
以GC Roots為起點,從這些起點開始向下搜索,經過的路徑稱為引用鏈。若一個對象到GC Roots之間沒有任何引用鏈,則該對象是不可達的。
在Java語言中,可作為GC Roots的對象包括以下幾種:
- 虛擬機棧(棧幀中的局部變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
在可達性分析過程中,對象引用類型會對對象的生命周期產生影響
JAVA中有這幾種類型的引用:
對象被回收的過程
- 當對象進行可達性分析沒有與GC Roots相連的引用鏈,將會被第一次標記,并根據是否需要執行finalize()方法進行一次篩選,對象沒有重寫finalize()或者虛擬機已經調用過finalize(),都被視為不需要執行
- ?如果對象有必要執行finalize,會被放入到F-Queue隊列中,并在稍后由虛擬機自動創建的低優先級的Finalizer線程去觸發它,并不保證等待此方法執行結束。
- 如果對象在finalize()方法執行中,重新和GC Roots產生了引用鏈,則可以逃脫此次被回收的命運,但finalize()方法只能運行一次,所以并不能通過此方法逃脫下一次被回收
- 不建議使用這個方法,建議大家完全忘掉這個方法的存在。
?方法區回收
主要包括廢棄常量和無用類的回收。
滿足以下三個條件,類才可以被回收(卸載):
- 類的實例都被回收,
- 類的ClassLoader被回收,
- 類的Java.Lang.Class對象沒有在任何地方引用。
?HotSpot虛擬機通過 -Xnoclassgc 參數進行控制是否啟用類卸載功能。在大量使用反射、動態代理、CGLib等框架,需要虛擬機具備類卸載功能,避免方法區發生內存溢出。
垃圾收集算法
標記-清除(Mark-Sweep)
分為兩個階段
- 標記:先標記出所有要回收的對象
- 清除:統一進行對象的回收
缺點
- 效率問題:標記和清除的效率都不高。
- 空間問題:會產生大量不連續的內存碎片,碎片太多會都導致大對象無法找到足夠的內存,從提前觸發垃圾回收。
復制(Copying)
把內存分成大小相同的兩塊,當一塊的內存用完了,就把可用對象復制到另一塊上,將使用過的一塊一次性清理掉
復制算法缺點:浪費了一半內存
標記-整理(Mark-Compact)
標記后,讓所有存活的對象移到一端,然后直接清理掉端邊界以外的內存
標記--整理算法分代收集
把堆分為新生代和老年代
- 新生代使用復制算法
將新生代內存分為一塊大的Eden區和兩塊小的Survivor;每次使用Eden和一個Survivor,回收時將Eden和Survivor存活的對象復制到另一個Survivor(HotSpot的比例Eden:Survivor = 8:1)??梢酝ㄟ^JVM參數調整。
- 老年代使用標記-清理或者標記-整理
HotSpot的算法實現
存活判定與收集算法僅是理論上,JVM實現時必須考慮效率,才能保證JVM高效運行。
枚舉根節點
GC Roots的節點主要在全局性引用(常量,靜態屬性)與執行上下文(本地變量表)中,逐個檢查消耗很多時間。
可達性分析對執行時間的敏感還表現在GC停頓上,GC時必須停頓所有Java執行線程(“STOP THE WORLD”)
?OopMap數據結構記錄哪些位置是引用。類加載完成時或JIT編譯時,GC時直接掃描OopMap。
安全點(Safepoint)
在OopMap協助下,HotSpot可以快速準確完成GC Roots枚舉。
特定位置記錄信息,稱為安全點。程序執行時并非在所有地方都可以停頓下來開始GC,只有在到達安全點時才能暫停。
安全點選定以程序“是否具有讓程序長時間執行的特征”為標準。“長時間執行”的最明顯特征是指令序列復用。例如方法調用,循環跳轉,異常跳轉。
如果在GC發生時讓所有線程(不包括執行JNI的線程)到最近的安全點上再停頓下來。2種方案:
- 搶先式中斷(Preemptive Suspension)
首選中斷所有線程,有線程未在安全點中斷,恢復線程運行到中斷點。(幾乎不采用)
-
主動式中斷(Voluntary Suspension)
需要GC時,不中斷線程,設置標志位,由線程主動輪詢,發現中斷標識時主動掛起。(輪詢標識的地方和安全點重合)。
?
安全區域(Safe Region)
程序不執行時,即沒有分配到CPU時間,線程處于Sleep或者Blocked狀態,線程無法響應JVM中斷,JVM也不會等待線程重新獲取CPU時間。對于這種情況采用安全區域來解決。
安全區域是指在一段代碼片段中,引用關系不會發生變化,在這個區域任何地方開始GC都是安全的,可看作擴展安全點。
在線程執行到安全區域時,首先標識自己已進入安全區域,GC時就不考慮這些線程了,在線程離開安全區域時,要檢查系統是否已完成根節點枚舉(或整個GC),如果完成了,線程就繼續執行,否則就必須等待接收到可以離開安全區域的信號。
垃圾收集器
收集算法是內存回收的理論,而垃圾回收器是內存回收的實踐。
如果兩個收集器之間存在連線說明他們之間可以搭配使用
Serial 收集器
這是一個單線程收集器。意味著它只會使用一個 CPU 或一條收集線程去完成收集工作,并且在進行垃圾回收時必須暫停其它所有的工作線程直到收集結束。
新生代采用復制算法
老年代采用標記-整理
ParNew 收集器
可以認為是 Serial 收集器的多線程版本。
并行:Parallel,指多條垃圾收集線程并行工作,此時用戶線程處于等待狀態
Parallel Scavenge 收集器
這是一個新生代收集器,也是使用復制算法實現,同時也是并行的多線程收集器。
CMS 等收集器的關注點是盡可能地縮短垃圾收集時用戶線程所停頓的時間,而 Parallel Scavenge 收集器的目的是達到一個可控制的吞吐量(Throughput = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))。
參數:
- -XX:MaxGCPauseMillis :最大GC停頓時間(毫秒)
- -XX:GCTimeRation:吞吐量大小(0~100)
作為一個吞吐量優先的收集器,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整停頓時間。這就是 GC 的自適應調整策略(GC Ergonomics)。
Serial Old 收集器
Serial 收集器的老年代版本,單線程,使用標記-整理。
這個收集器的主要意義:給Client模式下的虛擬機使用。
Server模式下,它還有2大用途:
- 與Parallel Scavenge收集器搭配使用
- 作為CMS收集器后備預案,在發生Concurrent Mode Failure時使用。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多線程,使用?標記-整理
CMS(Concurent Mark Sweep) 收集器
CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標的收集器?;?標記-清除?算法實現。
運作步驟:
缺點:
- 對 CPU 資源敏感。
默認回收線程數:(CPU數量+3) /4
- 無法收集浮動垃圾
第一次標記后出現的未處理垃圾稱為浮動垃圾,不能等到老年代填滿了再收集,如果預留內存無法滿足用戶線程需要,會產生Concurrent Mode Failure異常。
- 標記-清除算法帶來的空間碎片
更多細節參考圖解 CMS 垃圾回收機制原理
G1 收集器
優點:
- 并行與并發:通過并發方式讓Java程序繼續運行
- 分代收集
- 空間整合:全局上看采用標記-整理,從局部(2個Region之間)基于復制算法
- 可預測停頓:建立可停頓模型,讓使用者指定在長度M時間段內,GC不超過N。
其他收集器的范圍在某個代,G1不再這樣,它將整個內存分為多個大小相等的獨立區域(Region),雖然還保留新生代,老年代,但是新生代,老年代不再物理隔離,都是一部分Region(不需要連續)的結合。
運作步驟:
理解GC日志
閱讀 GC 日志是處理 JVM 內存問題的基礎技能,它只是一些人為確定的規則,每一種收集器的日志形式都是由它們自身的實現所決定的,但 JVM 設計者為了方便用戶閱讀,將各個收集器的日志都維持一定的共性,例如下面兩段典型的日志:
33.125: [GC [DefNew: 3324K->152K (3712K), 0.0025925secs] 3324K->152K (11904K), 0.0031680 secs]
100.667: [FullGC [Tenured: 0K->210K (10240K), 0.0149142secs] 4603K->210K (19456K), [Perm: 2999K->2999K (21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
數字 “33.125” 和 “100.667” 表示從 JVM 啟動以來經過的秒數。
“[GC” 和 “[Full GC” 說明了這次垃圾收集的停頓類型,而不是用來區分新生代 GC 還是老年代 GC 的。如果有 “Full”,說明這次 GC 是發生了 “Stop The World” 的,例如下面這段新生代收集器 ParNew 的日志也會出現 “[Full GC” (一般是因為出現了分配擔保失敗之類的問題,導致 STW)。如果是調用 System.gc() 方法所觸發的 GC,那么在這里將顯示 “[Full GC (System)”。
[Full GC 283.736: [ParNew: 261599K->261599K (261952K),0.0000288 secs]
“[DefNew”、”[Tenured”、”[Perm” 表示GC發生的區域,這里顯示的區域名稱與使用的 GC 收集器是密切相關的,例如上面樣例所使用的 Serial 收集器中的新生代名為 “Default New Generation”,所以顯示的是 “[DefNew”。
如果是 ParNew 收集器,新生代名稱就會變為 “[ParNew”。
如果是 Parallel Scavenge 收集器,新生代名稱就會變為 “PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
后面方括號內部的 “3324K->152K (3712K)” 即 “GC 前該內存區域已使用容量->GC 后該內存區域已使用容量 (該內存區域總容量)”。
而在方括號之外的 “3324K->152K (11904K)” 即 “GC 前 Java 堆已使用容量->GC 后 Java 堆已使用容量 (Java堆總容量)”。
再往后,”0.0025925 secs” 表示該內存區域 GC 所占用的時間,單位是秒。
有的收集器會給出更具體的時間數據,如 “[Times: user=0.01 sys=0.00,real=0.02 secs]”,這里面的 user、sys 和 real 與 Linux 的 time 命令所輸出的時間含義一致,分別代表用戶態消耗的CPU時間、內核態消耗的CPU事件和操作從開始到結束所經過的墻鐘時間 (Wall Clock Time)。
CPU 時間與墻鐘時間的區別是,墻鐘時間包括各種非運算的等待耗時,例如等待磁盤 I/O、等待線程阻塞,而 CPU 時間不包括這些耗時,但當系統有多 CPU 或者多核的話,多線程操作會疊加這些 CPU 時間,所以讀者看到 user 或 sys 時間超過 real 時間是完全正常的。
?
垃圾搜集器參數
垃圾搜集器選擇參數
| UseSerialGC | 開啟此參數使用serial & serial old搜集器(client模式默認值)。 | ? |
| UseParNewGC | 開啟此參數使用ParNew & serial old搜集器(不推薦)。 | ? |
| UseConcMarkSweepGC | 開啟此參數使用ParNew & CMS(serial old為替補)搜集器。 | ? |
| UseParallelGC | 開啟此參數使用parallel scavenge & parallel old搜集器(server模式默認值)。 | ? |
| UseParallelOldGC | 開啟此參數在年老代使用parallel old搜集器(該參數在JDK1.5之后已無用)。 | ? |
JVM各個內存區域大小相關參數
| Xms | 堆的初始值。默認為物理內存的1/64,最大不超1G。 | ? |
| Xmx | 堆的最大值。默認為物理內存的1/4,最大不超1G。 | ? |
| Xmn | 新生代的大小。 | ? |
| Xss | 線程棧大小。 | ? |
| PermSize | 永久代初始大小。默認為物理內存的1/64,最大不超1G。 | ? |
| MaxPermSize | 永久代最大值。默認為物理內存的1/4,最大不超1G。 | ? |
| NewRatio | 新生代與年老代的比例。比如為3,則新生代占堆的1/4,年老代占3/4。 | ? |
| SurvivorRatio | 新生代中調整eden區與survivor區的比例,默認為8,即eden區為80%的大小,兩個survivor分別為10%的大小。 | ? |
垃圾搜集器性能通用參數
| PretenureSizeThreshold | 晉升年老代的對象大小。默認為0,比如設為10M,則超過10M的對象將不在eden區分配,而直接進入年老代。 | ? |
| MaxTenuringThreshold | 晉升老年代的最大年齡。默認為15,比如設為10,則對象在10次普通GC后將會被放入年老代。 | ? |
| DisableExplicitGC | 禁用System.gc()。 | ? |
并行搜集器參數
| ParallelGCThreads | 回收時開啟的線程數。默認與CPU個數相等。 | ? |
| GCTimeRatio | 設置系統的吞吐量。比如設為99,則GC時間比為1/1+99=1%,也就是要求吞吐量為99%。若無法滿足會縮小新生代大小。 | ? |
| MaxGCPauseMillis | 設置垃圾回收的最大停頓時間。若無法滿足設置值,則會優先縮小新生代大小,仍無法滿足的話則會犧牲吞吐量。 | ? |
并發搜集器參數
| CMSInitiatingOccupancyFraction | 觸發CMS收集器的內存比例。比如60%的意思就是說,當內存達到60%,就會開始進行CMS并發收集。 | ? |
| UseCMSCompactAtFullCollection | 在每一次CMS收集器清理垃圾后送一次內存整理。 | ? |
| CMSFullGCsBeforeCompaction | 設置在幾次CMS垃圾收集后,觸發一次內存整理。 | ? |
內存分配與回收策略
對象優先在 Eden 分配
對象主要分配在新生代的 Eden 區上,如果啟動了本地線程分配緩沖區,將線程優先在 (TLAB) 上分配。少數情況會直接分配在老年代中。
Minor GC和Full GC
- 新生代 GC (Minor GC)。發生在新生代的垃圾回收動作,頻繁,速度快。Java對象大多都具備快速消亡特性。
- 老年代 GC (Major GC / Full GC):發生在老年代的垃圾回收動作,出現了 Major GC 經常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度一般會比 Minor GC 慢十倍以上。
大對象直接進入老年代
大對象指需要大量連續內存空間的對象,最典型的是很長的字符串以及數組。
-XX:PretenureSizeThreshold(只對Serial,ParNew有效)。大于此設置值的對象直接在老年代分配。
?
長期存活的對象將進入老年代
對象在Survivor中,每“熬過”一次Minor GC,年齡加1,當增加到一定程度(默認15),就會晉升到老年代。-XX:MaxTenuringThreshold設置此值。
?
動態對象年齡判定
虛擬機并不是永遠要求對象年齡達到MaxTenuringThreshold才晉升老年代,如果Survivor空間中相同年齡的所有對象大小超過Survivor的一半,年齡大于等于該年齡的對象直接進入老年代。
空間分配擔保
在發生Minor GC前,虛擬機會先檢查老年代最大可用連續空間是否大于新生代對象總空間,如果條件成立,那么Minor GC是安全的,如果不成立,虛擬機會查看HandlePromotionFailure設置是否允許擔保。如果允許,則繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,嘗試一次Minor GC,否則進行Full GC。
?
?
?
總結
以上是生活随笔為你收集整理的JVM-垃圾收集器与内存分配策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM-Java内存区域
- 下一篇: JVM-类文件结构