首頁>技術>

思維導圖前言

Java相對於C/C++語言來說,最明顯的特點在於Java引入了自動垃圾回收。垃圾回收(Garbage Collection簡稱GC)可以使程式設計師不在需要關心JVM記憶體管理的問題,專注於寫程式本身。平時程式設計師是很難感知到GC的存在,但是如果涉及到一些效能調優,線上的問題排查等等,深入地瞭解GC是必不可少的。往往透過一些JVM引數的設定就能使系統性能提高不少。

一、JVM記憶體區域

要深入瞭解GC,首先要明白GC會回收哪些資料,資料位於哪個區域。接著我們看一下JVM的記憶體區域。

從圖中可以看出,記憶體區域分為五個:

虛擬機器棧:執行緒私有,由一個個棧幀組成,每個棧幀對應著一個呼叫的方法,儲存有方法的區域性變數等資訊。方法被呼叫時棧幀入棧,方法結束呼叫時棧幀出棧。入棧出棧的時機很清楚,所以不需要進行GC。本地方法棧:與虛擬機器棧非常類似,本地方法棧與虛擬機器棧的區別在於,虛擬機器棧執行的是Java方法,本地方法棧執行的是本地方法(Native Method)。這塊區域也不需要進行GC。程式計數器:執行緒私有的,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。我們知道JVM的多執行緒是透過CPU時間片輪轉(即執行緒輪流切換並分配處理器執行時間)演算法來實現的。也就是說,某個執行緒在執行過程中可能會被掛起,而另一個執行緒獲取到時間片開始執行。在JVM中,就是透過程式計數器來記錄某個執行緒的位元組碼執行位置,當被掛起的執行緒重新獲取到時間片的時候,就知道上次被掛起時執行到哪個位置了。這塊區域也不需要GC。方法區:在Java8之前有永久代的概念,在堆中實現,受GC的管理,主要儲存類的資訊,常量,靜態變數,由於永久代有 -XX:MaxPermSize 的上限,所以很容易造成 OOM。在Java8之後,永久代被移除,然後把方法區的實現移到了本地記憶體中的元空間中,這樣方法區就不受 JVM 的控制了。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。所以Java8以後,方法區也不需要GC。堆:堆是Java物件的儲存區域,任何new欄位分配的Java物件例項和陣列,都被分配在了堆上。GC主要作用於這個區域,對這兩類資料進行回收。二、如何判斷物件是否可回收

上面講了GC主要作用的區域是在堆中,那麼又是怎麼判斷是否可以回收的呢?在GC裡面有兩種演算法來判斷,一種是引用計數,物件引用的次數為0就是垃圾,另一種是可達性演算法,如果一個物件不在以GC Root根節點為起點的引用鏈中,則視為垃圾。

2.1 引用計數演算法

首先看引用計數法,簡單點說物件被引用,就會在此物件的物件頭上計數器加一,每當有一個引用失效時計數器的值減一,如果沒有引用(引用次數為0)則此物件可回收。但是這種演算法很難解決物件之間互相迴圈引用的問題。

2.2 可達性演算法

所謂的GC Roots就是一組必須活躍的引用,基本思路就是從一系列的GC Root一直往下搜尋,透過GC Root串成的一條線稱為引用鏈,如果有物件不在任何一條以GC Root為起點的引用鏈中,則此物件就會被GC回收,這就是可達性演算法。

哪些物件可作為GC Root物件呢:

虛擬機器棧(棧幀中的本地變量表)中引用的物件方法區中類靜態屬性引用的物件方法區中常量引用的物件本地方法棧中 JNI(即一般說的 Native 方法)引用的物件三、常見的垃圾回收演算法

上面已經講了如何判斷哪些物件是可回收的。那麼判斷完是否可回收後,GC又是使用什麼演算法進行回收的呢?這就要講一講垃圾回收的幾種方式:

標記清除法標記整理法複製演算法分代收集演算法3.1 標記清除法

其實很簡單,分為標記清除兩個步驟。第一步根據可達性演算法標記被回收的物件,第二步回收被標記的物件。

明顯這種垃圾回收演算法的缺點是很容易產生記憶體碎片。

3.2 標記整理法

前面兩個步驟和標記清除演算法一樣,而不同的是在標記清除演算法的基礎上多了一步整理的過程。如圖所示,整理步驟的時候,將所有存活的物件都往左邊移動,然後清理另一端的所有區域,這樣就不會產生記憶體碎片。

雖然不會產生記憶體碎片,但是由於頻繁地移動存活的物件,所以效率十分低下。

3.3 複製演算法

把記憶體分成兩份,分別是A區域和B區域,第一步根據可達性演算法把存活的物件標記出來,第二步把存活的物件複製到B區域,第三步把A區域全部清空。這就是複製演算法。

複製演算法不會產生記憶體碎片,並且不需要頻繁移動存活的物件,而缺點就是記憶體利用不充分,比如一塊500M的記憶體,要分成兩份,只能利用到250M。

3.4 分代收集演算法

分代蒐集演算法是針對物件的不同特性,而使用適合的演算法,這裡面並沒有實際上的新演算法產生。與其說分代收集演算法是第四個演算法,不如說它是對前三個演算法的實際應用。

首先我們先探討一下物件的不同特性,記憶體中的物件其實可以根據生命週期的長短大致分為三種:

夭折物件(新生代):朝生夕死的物件,比如方法裡的區域性變數。持久物件(老年代):存活的比較久但還是要死的物件,比如快取物件,單例物件等等。永久物件(永久代):物件生成後幾乎不滅的物件,例如String池中的物件(享元模式)、載入過的類資訊等等。

上述的物件對應在記憶體中的區域就是,夭折物件和持久物件在Java堆中,永久物件在方法區。

分代演算法的原理就是根據物件的存貨週期不同將堆分為年輕代和老年代。新生代又分為Eden 區,from Survivor 區(S0區),to Survivor 區(S1區),比例為8:1:1。

先看年輕代的GC,年輕代採用的回收演算法是複製演算法。新建的物件被建立後就會分配在Eden 區,當Eden區將滿時,就會觸發GC。

在這一步GC會把大部分夭折物件回收,根據可達性演算法標記出存活的物件,把存活物件複製到S0區,然後清空Eden 區。

接著繼續到下一次觸發GC時,就會把Eden區和S0區的存活物件複製到S1區,然後清空Eden區和S0區。每次垃圾回收後S0和S1區的角色互換。每次GC後,如果物件存活下來則年齡加一。

我們知道在年輕代中存活得越久的物件,年齡會越大,如果存活物件的年齡達到了我們設定的閾值,則會從S0(或S1)晉升到老年代。由於老年代的物件一般不會經常回收,所以採用的演算法是標記整理法,老年代的回收次數相對較少,每次回收時間比較長。

四、Stop the world

Java中Stop The World機制簡稱STW,執行垃圾收集演算法時,Java應用程式的其他所有執行緒都被掛起(除了垃圾收集器之外),當垃圾回收完成後,再繼續執行,所以儘量減少STW的時間,就是最佳化JVM的主要目標。

五、常見的垃圾收集器

垃圾收集器其實就是上面講的演算法的具體實現,目前沒有說哪個垃圾收集器是最好的,只有根據應用的特點選擇最合適的,所以說合適的才是最好的。

常見的垃圾收集器除了G1垃圾收集器外,都是隻作用於一個區域,要麼年輕代要麼老年代,所以一般是配合使用,總共有7種,怎麼配合使用,請看下面這張圖,有連線的就是可以配合使用的。

5.1 Serial收集器

Serial收集器作用於年輕代,單執行緒的垃圾收集器,單執行緒意味著它只會使用一個CPU或者一個執行緒去完成垃圾回收的工作,當它在垃圾回收時,由於SWT機制,其他工作執行緒都會被暫時掛起,直到垃圾回收完成。這種垃圾收集器適用於Client模式的應用,在單CPU的環境下,由於沒有和其他執行緒互動的開銷,可以專心垃圾回收的工作,能夠把單執行緒的優勢發揮到極致,簡單高效。透過-XX:+UseSerialGC可以開啟這種回收模式。

5.2 ParNew收集器

ParNew 收集器是Serial收集器的多執行緒版本,作用於年輕代,預設開啟的收集執行緒數和cpu數量一樣,執行數量可以透過修改ParallelGCThreads設定。

5.3 Parallel Scavenge收集器

Parallel Scavenge收集器也被稱為吞吐量優先收集器,作用於年輕代,多執行緒採用複製演算法的垃圾收集器,跟ParNew 收集器有些類似。和ParNew 收集器不同的是,Parallel Scavenge收集器關注的是吞吐量,它提供了兩個引數來控制吞吐量,分別是-XX:MaxGCPauseMillis(控制最大的垃圾收集停頓時間)、 -XX:GCTimeRatio(直接設定吞吐量大小)。

如果設定了-XX:+UseAdaptiveSizePolicy引數,虛擬機器就會根據系統的執行情況收集監控資訊,動態調整新生代的大小,Eden,Survivor比例等,以儘可能地達到我們設定的最大垃圾收集時間或吞吐量大小這兩個指標,這種調節方式稱為GC的自適應調節策略。這也是Parallel Scavenge收集器和ParNew 收集器最大的區別。

5.4 Serial Old收集器

Serial Old 收集器是工作在老年代的單執行緒垃圾收集器,採用的演算法是標記整理演算法。在Client模式下可以和Serial收集器配合使用,如果在Server模式的應用,在JDK1.5之前可以和Parallel Scavenge收集器配合使用,另一種使用場景則是CMS垃圾收集器的後備預案,在發生Concurrent Mode Failure使用。

5.5 Parallel Old收集器

Parallel Old 收集器是Parallel Scavenge收集器的老年代版本,多執行緒收集,採用標記整理演算法。下圖是Parallel Scavenge收集器和Parallel Old 收集器配合工作的過程圖。

5.6 CMS收集器

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,採用標記-清除演算法。適用於希望系統停頓時間短,給使用者更好的體驗的場景。

CMS收集器執行時主要分為四個步驟:

初始標記:標記GC Roots能直接關聯的物件。存在Stop The World。併發標記:GC Roots Tracing,可以和使用者執行緒併發執行。重新標記:標記期間產生的物件存活的再次判斷,修正對這些物件的標記,執行時間相對併發標記短,存在Stop The World。併發清除:清除物件,可以和使用者執行緒併發執行。

CMS收集器的缺點在於:

對CPU資源比較敏感。無法處理浮動垃圾。可能出現 「Concurrent Mode Failure」而導致另一次 Full GC 的產生,由於在併發清理時使用者執行緒還在執行,所以清理垃圾同時新的垃圾也會不斷產生,這部分垃圾(即浮動垃圾)只能在下一次 GC 時再清理掉。採用的是標記清除演算法,所以會產生記憶體碎片。記憶體碎片會導致大物件無法分配到連續的記憶體空間,然後會產生Full GC,影響應用的效能。5.7 G1收集器

G1垃圾回收器主要是面向服務端的垃圾回收器,年輕代和老年代都可使用。運作時,整體上採用標記整理演算法,區域性上看是採用複製演算法,兩種演算法都不會產生記憶體碎片,所以回收器在回收後能產生連續的記憶體空間。

它是專門針對以下場景設計的:

像CMS收集器一樣,能與應用程式執行緒併發執行。整理空閒空間更快。需要GC停頓時間更好預測。不希望犧牲大量的吞吐效能。不需要更大的Java Heap。

G1垃圾回收器的記憶體分割槽不再採用傳統的記憶體分割槽,將新生代,老年代的物理空間劃分取消了。

取而代之的是,把堆記憶體分成若干個Region(區域),每次收集的時候,只收集其中幾個區域,以此來控制垃圾回收產生的STW。G1垃圾回收器和傳統的垃圾回收器的最大區別就在於,弱化了分代概念,引入了分割槽的思想

G1中每代的儲存地址都不是連續的,而是使用了不連續的大小相同的Region。除此之外G1中還多了一個H,H代表Humongous,用於儲存巨大物件(humongous object),當物件大小大於等於region一半的物件,就直接分配到了老年代,防止了反覆複製移動。

G1垃圾回收過程可分為四步:

初始標記。收集所有GC根(物件的起源指標,根引用),STW,在年輕代完成。併發標記。標記存活物件。最終標記。是最後一個標記階段,STW,很短,完成所有標記工作。篩選回收。回收沒有存活物件的Region並加入可用Region佇列。總結

本文的簡述了JVM的垃圾回收的理論知識,思路是先搞懂GC作用的區域是在堆中,然後介紹可達性演算法的作用是為了標記存活的物件,知道哪些是可回收物件,接著就是使用垃圾回收演算法進行回收,然後介紹了常見的幾種垃圾回收演算法(標記清除,複製演算法,標記整理),最後再介紹常見的幾種垃圾回收器。

對於垃圾回收器的介紹,這裡只是簡單的描述,並沒有深入地講解,因為每一個垃圾回收器如果展開細述都能講上半天,所以有興趣的話,可以自己再去探索一下,個人認為CMS和G1垃圾回收器是比較重要的兩種。

我是一個努力讓大家記住的程式設計師。我們下期再見!!!

能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!

11
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 每日一問:你想如何破壞單例模式?