垃圾收集器和記憶體分配策略 JVM筆記2
目錄
程式計數器,虛擬機器棧,本地方法棧這3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨方法進入和退出有條不紊地執行出棧和入棧操作。每一棧幀中分配的記憶體基本上從類結構確定下來就已知。所以這幾個區域就不需要過多考慮回收
垃圾回收器
重點關注的是這裡的記憶體。
物件已死嗎
垃圾回收器對堆進行回收前,第一時間就是要確定這些物件之中哪些還“存活”著。
引用計數演算法
方法:給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;引用失效則減1;任何時刻計數器為0的物件就是不可能再被使用的。缺點
:不能解決相互迴圈引用問題。
public class Sample{ public Object instance = null; private static final int _1MB = 1024*1024; } /** *這個成員的作用就是佔點記憶體 */ private byte[] BigSize = new byte[2 * _1MB]; public static void testGC(){ Sample objA = new Sample(); Sample objB = new Sample(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); /** *事實上是,在這之後,objA和objB被回收了, *因此我們可以確定Java並不是用的引用計數演算法 */ }
可達性分析演算法
現在的主流應用程式語言(包括Java)都是通過可達性分析(Reachability Analysis)來判定物件是否存活的。
方法:通過一系列稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑被稱為引用鏈
,當一個物件到GC Roots沒有任何引用鏈相連時,證明此物件不可用。
以下幾種物件可以作為GC Roots:
- 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
- 方法區中類靜態屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中JNI(Native方法)引用的物件。
再談引用
Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference),虛引用(Phantom Reference)4種,引用強度依次遞減。
- 強引用指程式程式碼中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的物件。
- 軟引用用來描述一些還有用但非必需的物件。在系統快要發生
記憶體溢位
異常之前,將會將這些物件進行第二次回收,如果還沒有足夠記憶體,會丟擲記憶體溢位異常。 - 弱引用關聯物件只能生存到
下一次垃圾收集
發生之前。 - 虛引用是最弱的引用關係,一個物件是否有虛引用對自身完全不構成影響,也無法通過虛引用獲得例項。僅用來能在引用虛引用的物件被收集器回收時受到一個系統通知。
生存還是死亡
真正宣告一個物件的死亡要經過兩次
標記過程:
- 如果物件在可達性分析後發現沒有與GC Roots相連的引用鏈,那麼它將會被第一次標記並且進行一次篩選。
- 篩選的條件是此物件是否有必要執行
finalize()
方法。當物件沒有覆蓋finalize方法,或者finalize方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。 - 如果一個物件被視為“有必要執行”finalize方法,那麼物件將會被放置到一個叫做F-Queue的佇列中。並在稍後由一個虛擬機器自動建立、低優先順序的Finalizer執行緒去執行它。這裡的執行不代表會等待執行結束,如果一個物件執行緩慢或者發生死迴圈就是另外一種情況了。
- 在執行finalize方法的過程中,是物件
最後一次
拯救自己的機會,物件如果在finalize方法中重新與引用鏈上的一個物件建立關聯,則被移除出即將回收的集合。如果這個時候還沒有建立關聯,那麼它就真的被回收了。需要注意的是,finalize方法對於任何物件來說都只會被系統自動呼叫僅一次
。並且不建議在程式碼的編寫中,用這個方法。
回收方法區
永久代主要回收兩部分內容:廢棄常量和無用的類。
- 回收廢棄常量的方法與Java堆中的物件類似。比如一個字串“abc”已經進入常量池,而在當前系統沒有任何一個String物件叫做“abc”,換句話說,沒有任何一個String物件引用常量池中的“abc”常量,也沒有其他地方引用這個字面量,這個時候進行記憶體回收,如果有必要,它將會被清理出常量池。
- 判定一個類是否為無用的類,條件就要苛刻了許多。要
同時滿足
下面三個條件。- 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
垃圾回收演算法
標記-清除演算法
思路:首先標記所有需要回收的方法,然後在標記完成後統一回收所有被標記的物件。缺點
:標記和清除兩個過程的效率都不高;標記清除後,會產生大量不連續的記憶體碎片,空間碎片太多會導致以後在程式執行過程中需要分配大的物件時,無法找到匹配大小的連續記憶體,從而不得不提前觸發另一次垃圾回收操作。
複製演算法
思路:將可用記憶體分為大小相等的兩塊,每次只使用其中的一塊。當一塊的記憶體用完了,就將還活著的物件複製到另一塊上面,然後把已使用的記憶體空間清理掉。注意
:
- 當前的商業虛擬機器都採用這種收集方法收集新生代。
- 實際運用中,並不是按照1:1的方法,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。預設比例為8:1:1.
標記-整理演算法
-
思路:標記清除的過程仍然與標記-清除演算法一樣,但後續是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體。
注意
: - 在老年代一般選用這種演算法
分代收集演算法
當前商業虛擬機器都採用分代收集演算法(Generational Collection)演算法,這種演算法根據物件的存活週期將記憶體劃分為幾塊。一般是將物件分為新生代和老年代。
新生代:每次收集都發現有大批物件死去,只有少量存活,那就選用複製演算法。
老年代:存活率高,沒有額外空間對它進行擔保,就必須選擇標記-整理或者標記-清理演算法來進行回收。
HotSpot的演算法實現
- 可作為GC Roots的節點主要在
全域性性的引用
(例如常量或類靜態屬性)與執行上下文
(例如棧幀中的本地變量表中)。 - GC進行時必須
停頓
所有Java執行執行緒。 - 在HotSpot的是實現中,使用一組稱為OopMap的資料結構來得知哪些地方存放著物件引用。
- 程式只在特定的地方停頓下來開始GC,只有在到達
安全點
時才能暫停. - 安全點以程式是否具有讓程式長時間執行的特徵為標準進行選定的。例如方法呼叫、迴圈跳轉、異常跳轉等。
- 讓執行緒都跑到安全點上停頓下來,有
搶先式中斷
和主動式中斷
兩種方法。 - 假如執行緒處於Sleep或者Blocked狀態,就用
安全區域
來解決。
垃圾收集器
HotSpot虛擬機器的所有收集器如圖所示。
如果兩個收集器之間存在連線,就說明它們可以搭配使用。所處區域代表它是屬於老年代收集器還是新生代收集器。
Serial收集器
- 是一個
單執行緒
的收集器。 - 進行垃圾收集時,必須暫停其他所有的工作程序,直到它收集結束。
- 對於執行在Client模式下的虛擬機器來說時一個很好的選擇。
ParNew收集器
- 其實就是Serial收集器的
多執行緒
版本 - 隨著可用CPU的數量的增加,它對於GC時系統資源的有效利用很有好處。
- 是許多執行在Server模式下的虛擬機器首選的新生代收集器。
Parallel Scavenge 收集器
- 是一個新生代收集器,也是使用複製演算法的收集,又是並行的多執行緒收集器。
- 設計的目標是為了達到一個可控制的
吞吐量
(CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值)。 - Parallel Scavenge收集器提供兩個引數用於精確控制吞吐量。分別是控制最大垃圾收集停頓時間的
-XX:MaxGCPauseMillis
引數以及直接設定吞吐量大小的-XX:GCTimeRatio
引數。 -XX:+UseAdaptiveSizePolicy
引數用於根據當前系統執行情況收集效能監控資訊,動態調整新生代大小、Eden與Survivor區比例、晉升老年代物件大小等細節引數。這種調節方式被稱為GC自適應
的調節策略(GC Ergonomics)。
Serial Old收集器
- 是Serial收集器的老年代版本,同樣是一個單執行緒收集器,使用標記-整理演算法。
- 主要意義在於給Client模式下的虛擬機器使用。
- 如果在Server模式下主要有兩大用途:
- 在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用。
- 作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。
Parallel Old收集器
- 是Parallel Scavenge收集器的老年版本,使用多執行緒和標記-整理演算法。
- 在JDK 1.6中才開始提供。
- 直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以考慮Parallel Scavenge收集器加Parallel Old收集器。
CMS收集器
- CMS(Concurrent Mark Sweep)收集器是一個以獲取最短回收停頓時間為目標的收集器。
- CMS是基於“標記-清除”演算法實現的,它的運作過程分為4個步驟:
- 初始標記。標記GC Roots能夠直接關聯到的物件,速度很快。
- 併發標記。進行GC Roots Tracing的過程。
- 重新標記。為了修正併發標記因使用者程式繼續運作而導致標記產生變動的那一部分物件的記錄。
- 併發清除。
- 其中,初始標記、重新標記這兩個步驟,仍然需要暫停其他執行緒。由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,因此從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起
併發執行
的。 - 有併發收集、低停頓的優點。但是卻有三個明顯的缺點:
- 對CPU資源非常敏感。
- 無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。不能像其他老年代在等到快要被填滿了再收集。
- 會有大量空間碎片產生,給大物件的分配帶來很大麻煩。因此CMS收集提供了開關引數
-XX:+UseCMSCompactAtFullCollection
(預設開啟),用於在CMS頂不住進行FullGC時開啟記憶體碎片的合併整理過程,但停頓時間不得不變長。還有引數-XX:CMSFullGCsBeforeCompaction
,這個引數用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設為0,表示每次進入都進行碎片整理)。
G1收集器
- 是當今收集器技術發展的最前沿成果之一,早在JDK1.7確立專案目標。
- 是一款面向服務端應用的垃圾收集器。特點如下:
- 並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World的時間。
- 分代收集:G1可以不需要其他收集器的配合就獨立管理整個GC堆,但它能採用不同的方式區處理不同物件。
- 空間整合:G1從整體上來看是基於“標記-整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的。不會產生記憶體空間碎片。
- 可預測的停頓:除了追求低停頓外,還能建立可預測的停頓時間模型,讓使用者明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
- 它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離了,而是一部分Region(不需要連續)的集合。
內部分配與回收策略
- 大多數情況下,物件在新生代Eden區中分配。當Eden區沒有足夠空間時,虛擬機器將發起一次Minor GC(新生代GC)。
大物件
直接進入老年代。大物件是指需要大量連續記憶體空間的Java物件,比如很長的字串以及陣列。虛擬機器提供-XX:PretenureSizeThreshold
引數,令大於這個設定值的物件直接在老年代分配。這樣做是為了避免在Eden區及兩個Survivor區之間大量的記憶體複製。長期存活的物件
將進入老年代。虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1.每熬過一次Minor GC,年齡就加1歲當他的年齡增加到一定長度(預設15),將會被晉升到老年代。虛擬機器提供-XX:MaxTenuringThreshold
設定年齡閾值。- 動態物件年齡判定:為了更好適應不同程式的記憶體情況,虛擬機器並不是永遠地要求年齡必須達到MaxTenuringThreshold才進入老年代。如果在Survivor空間中相同年齡所有物件大小總和大於Survior空間的一般,年齡大於或等於該年齡的物件都可以直接進入老年代。
- 空間分配擔保:當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。在發生Minor GC之前會先檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試進行一次Minor GC,儘管是有風險的;如果小於,或者設定不允許冒險,那這時也要改為進行一次Full GC。