JVM GC 機制
1. 在此以前須要瞭解相關概念:
1.1 Java 堆內存:
在 HotSpot JVM 實現中 Heap 內存被"分代" 管理。
JVM 的內存首先被分割成兩部分:
-Heap Memory 堆內存
堆內存是咱們程序運行時能夠申請的內存空間,用於存儲程序運行時的數據信息。
-Non Heap Memory 非堆內存
除了堆內存區域用來存放存活(Living)的數據,JVM 還須要尤爲是類描述、元數據等更多信息。因此這寫信息統一被存放命名爲 Permanent generation(永久/常駐代)的區域。
非堆內存其實就是 JVM 留給本身用的,因此方法區、JVM 內部處理或優化所須要的內存(如 JIT 編譯後的代碼緩存區)、每一個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼等都在非堆內存中。
非堆內存由 JVM 管理,咱們沒法在程序中使用。
Heap Memory 又被分爲兩大區域:
-Young/New Generation 新生代
新生對象放置在新生代中,新生代由 Eden 與 Survivor Space 組成。
-Old/Tenured Generation 老年代
老年代用存放程序中通過幾回垃圾回收還存活的對象。
複製代碼
1.1.1 Young/New Generation 新生代:
程序中新建的對象都將分配到新生代中,新生代又由 Eden(伊甸園)與兩塊 Survivor(倖存者) Space 構成。Eden 與 Survivor Space 的空間大小比例默認爲 8 :1 ,即當 Young/New Generation 區域的空間大小總數爲 10 M 時,Eden 的空間大小爲 8 M,兩塊 Survivor Space 則各分配1 M,這個比例能夠經過 -XX:SurvivorRatio 參數來修改。Young/New Generation 的大小則能夠經過 -Xmn 參數來指定。
Eden:剛剛新建的對象將會被放置到 Eden 中,這個名稱寓意着對象們能夠在其中快樂自由的生活。
Survivor Space:倖存者區域是新生代與老年代的緩衝區域,兩塊倖存者區域分別爲 s0 與 s1,當觸發Minor GC 後將仍然存活的對象移動到S0中去(From Eden To s0)。這樣 Eden 就被清空能夠分配給新的對象。
當再一次觸發 Minor GC 後,S0 和 Eden 中存活的對象被移動到 S1 中(From s0To s1),S0 即被清空。在同一時刻, 只有 Eden 和一個 Survivor Space 同時被操做。因此 s0 與 s1 兩塊 Survivor 區同時會至少有一個爲空閒的,這點從下面的圖中能夠看出。
當每次對象從 Eden 複製到 Survivor Space 或者從 Survivor Space 之間複製,計數器會自動增長其值。 默認狀況下若是複製發生超過 16 次,JVM 就會中止複製並把他們移到老年代中去。若是一個對象不能在Eden 中被建立,它會直接被建立在老年代中。
新生代 GC(Minor GC):指發生在新生代的垃圾收集動做,由於 Java 對象大多都具有朝生夕滅的特性,一般不少的對象都活不過一次 GC,因此 Minor GC 很是頻繁,通常回收速度也比較快。
Minor GC 清理過程(圖中紅色區域爲垃圾):
1 、清理以前,看下圖
複製代碼
2 、清理以後,看下圖
複製代碼
注意:圖中的"From" 與"To" 只是邏輯關係而不是 Survivor Space 的名稱,也就是說誰裝着對象誰就是"From" 。
一個對象在倖存者區被移動/複製的次數決定了它是否會被移動到堆中。
複製代碼
1.1.2 Old/Tenured Generation 老年代
老年代用於存放程序中通過幾回垃圾回收後還存活的對象,例如緩存的對象等,老年代所佔用的內存大小即爲 -Xmx 與 -Xmn 兩個參數之差。
堆是 JVM 中全部線程共享的,所以在其上進行對象內存的分配均須要進行加鎖,這也致使了 new 對象的開銷是比較大的,鑑於這樣的緣由,Hotspot JVM 爲了提高對象內存分配的效率,對於所建立的線程都會分配一塊獨立的空間,這塊空間又稱爲 TLAB(Thread Local Allocation Buffer),其大小由 JVM 根據運行的狀況計算而得,在 TLAB 上分配對象時不須要加鎖,所以 JVM 在給線程的對象分配內存時會盡可能的在 TLAB 上分配,在這種狀況下 JVM 中分配對象內存的性能和 C 基本是同樣高效的,但若是對象過大的話則仍然是直接使用堆空間分配,TLAB 僅做用於新生代的 Eden,所以在編寫 Java 程序時,一般多個小的對象比大的對象分配起來更加高效,但這種方法同時也帶來了兩個問題,一是空間的浪費,二是對象內存的回收上仍然無法作到像 Stack 那麼高效,同時也會增長回收時的資源的消耗,可經過在啓動參數上增長 -XX:+PrintTLAB 來查看 TLAB 這塊的使用狀況。
老年代GC(Major GC/Full GC):
指發生在老年代的 GC,出現了 Major GC,一般會伴隨至少一次Minor GC(但也並不是絕對,在 ParallelScavenge 收集器的收集策略裏則可選擇直接進行 Major GC)。Major GC 的速度通常會比 Minor GC 慢10 倍以上。
虛擬機給每一個對象定義了一個對象年齡(age)計數器。若是對象在 Eden 出生並通過第一次 Minor GC 後仍然存活,而且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1 。對象在 Survivor 區中每熬過一次 Minor GC,年齡就增長 1 歲,當它的年齡增長到必定程度(默認爲 15 歲)時,就會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 來設置。
當一個 Object 被建立後,內存申請過程以下:
1 .JVM 會試圖爲相關 Java 對象在 Eden 中初始化一塊內存區域。
2 .當 Eden 空間足夠時,內存申請結束。不然進入第三步。
3 .JVM 試圖釋放在 Eden 中全部不活躍的對象(這屬於1 或更高級的垃圾回收), 釋放後若 Eden 空間仍然不足以放入新對象,則試圖將部分 Eden 中活躍對象放入 Survivor 區。
4 .Survivor 區被用來做爲新生代與老年代的緩衝區域,當老年代空間足夠時,Survivor 區的對象會被移到老年代,不然會被保留在 Survivor 區。
5 .當老年代空間不夠時,JVM 會在老年代進行0 級的徹底垃圾收集(Major GC/Full GC)。
6 .Major GC/Full G 後,若 Survivor 及老年代仍然沒法存放從 Eden 複製過來的部分對象,致使JVM 沒法在 Eden 區爲新對象建立內存區域,JVM 此時就會拋出內存不足的異常。
複製代碼
2. GC
2.1 什麼是 GC
每一個程序員都遇到過內存溢出的狀況,程序運行時,內存空間是有限的,那麼如何及時的把再也不使用的對象清除將內存釋放出來,這就是 GC 要作的事。
理解 GC 機制就從:"GC 的區域在哪裏" ,"GC 的對象是什麼" ,"GC 的時機是什麼" ,"GC 作了那些事" 幾方面來分析。
複製代碼
2.2 須要 GC 的內存區域
JVM 中,程序計數器、虛擬機棧、本地方法棧都是隨線程的而生隨線程而滅,棧幀隨着方法的進入和退出作入棧和出棧操做,實現了自動的內存清理,所以,咱們的內存回收主要集中於 Java 堆和方法區中,在程序運行期間,這部份內存的分配和使用都是動態的。
複製代碼
2.3 GC 的對象
須要進行回收的對象就是已經沒有存活的對象,判斷一個對象是否存活經常使用的兩種辦法:
1 、引用計數:
每一個對象有一個引用計數屬性,新增一個引用時計數加 1 ,引用釋放時計數減 1 ,計數爲 0 時能夠回收。此方法簡單,沒法解決對象相互循環引用的問題。
2 、可達性分析:
從 GC Roots 開始向下搜索,搜索所走過的路徑成爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。不可達對象。
在 Java 語言中,GC Roots 包括:
-虛擬機棧中的引用對象。
-方法區中類靜態屬性實體引用的對象。
-方法區中常量引用的對象。
-本地方法棧中 JNI 引用的對象。
複製代碼
2.4 何時觸發 GC
1 、程序調用 System.gc 時能夠觸發
2 、系統自身來決定 GC 觸發的時機(根據 Eden 區和 From Space區的內存大小來決定。當內存大小不足時,則會啓動 GC 線程並中止應用線程)。
GC 有分爲 Minor GC 和 Full GC(也稱爲 Major GC)
-Minor GC 觸發條件:
當 Eden 區滿時,觸發 Minor GC
-Full GC 觸發條件:
a.調用 System.gc 時,系統建議執行 Full GC,可是沒必要然執行
b.老年代空間不足
c.方法區空間不足
d.經過 Minor GC 後進入老年代的平均大小大於老年代的可用內存
e.由 Eden 區、From Space 區向 To Space 區複製時,對象大小大於 To Space 可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
複製代碼
2.5 GC 作了什麼事
主要作了清理對象,整理內存的工做。Java 堆分爲新生代和老年代,採用了不一樣的回收方式。(回收方式即回收算法)
複製代碼
3. GC 垃圾回收算法
3.1 GC 經常使用算法
GC 經常使用的算法有:
1 、標記-清除算法
2 、複製算法
3 、標記-壓縮算法
4 、分代收集算法
目前主流的 JVM(HotSpot)採用的是分代收集算法。
複製代碼
3.1.1 標記 - 清除算法:
如它的名字同樣,算法分爲"標記" 和"清除" 兩個階段。
首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其缺點進行改進而獲得的。
它的主要缺點有兩個:
1 、效率問題,標記和清除過程的效率都不高。
2 、空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使當前程序在之後的運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發一次垃圾收集動做。
複製代碼
3.1.2 複製算法:
"複製" (Copying)的收集算法,它將可用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對其中的一塊就行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則致使效率下降,
複製代碼
3.1.3 標記 - 壓縮算法:
複製收集算法在對象存活率較高時就要執行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費 50 % 的空間,就須要有額外的空間進行分配擔保。以應對被使用的內存中全部對象都 100 % 存活的極端狀況,因此在老年代通常直接選用這種算法。
根據老年代的特色,有人提出了另一種"標記-整理" (Mark-Compact)算法,標記過程仍然與"標記-清除" 算法同樣,但後續的步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉一端邊界之外的內存。
複製代碼
3.1.4 分代收集算法
GC 分代的基本假設:
絕大部分對象的生命週期都很是短暫,存活時間短。
"分代收集" (Generational Collection)算法,把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
在新生代中,每次垃圾收集時都會發現有大批的對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
而在老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用"標記-清理" 或"標記-整理" 算法來進行回收。
複製代碼