首頁  >  文章  >  Java  >  JVM的記憶體區域劃分以及垃圾回收機制詳解

JVM的記憶體區域劃分以及垃圾回收機制詳解

巴扎黑
巴扎黑原創
2017-06-23 15:11:511687瀏覽

在我們寫Java程式碼時,大部分情況下是不用關心你New的物件是否被釋放掉,或是何時被釋放掉。因為JVM中有垃圾自動回收機制。在之前的部落格中我們聊過Objective-C中的MRC(手動引用計數)以及ARC(自動引用計數)的記憶體管理方式,下方會對其進行回顧。而目前的JVM的記憶體回收機制則不是使用的引用計數,而是主要使用的「複製式回收」和「自適應回收」。

當然除了上面是這兩種演算法外,還有其他是演算法,下方也會介紹。這篇博客,我們先簡單聊聊JVM的區域劃分,然後在此基礎上介紹一下JVM的垃圾回收機制。

 

一、JVM記憶體區域分割簡述

##當然本部分簡單的聊一下JVM的記憶體區域的劃分,為下方垃圾回收機制內容的展開進行鋪墊。當然對JVM記憶體區域劃分的內容網上有很多詳細的內容,請自行Google。

根據JVM記憶體區域的劃分,簡單的畫了下方的這個示意圖。區域主要分為兩大塊,一塊是

堆區(Heap),我們所New出的物件都會在堆區進行分配,在C語言中的malloc所分配的方法就是從Heap區獲取的。而垃圾回收器主要是將堆區的記憶體回收的。

而另一部分則是

非堆區,非堆區主要包括用於編譯和保存本機程式碼的「程式碼快取區(Cache)」、保存JVM自己的靜態資料的「永生代(Perm Gen)」、存放方法參數局部變數等引用以及記錄方法呼叫順序的「Java虛擬機器堆疊(JVM Stack) 」和「本機方法堆疊(Local Method Stack)」。

  

垃圾回收器主要回收的是堆區中未使用的記憶體區域,並對對應的區域進行整理。在堆區中,又根據物件記憶體的存活時間或物件大小,分為「

年輕代」和「年老代」。 「年輕代」中的物件是不穩定的容易產生垃圾,而「年老代」中的物件比較穩定,不易產生垃圾。之所以將其分開,是分而治之,根據不同區域的記憶體區塊的特點,採取不同的記憶體回收演算法,從而提高堆區的垃圾回收的效率。下方會給予具體的介紹。

 

 

二、常見的記憶體回收演算法簡介

上面我們簡單的了解的

JVM中記憶體區域的劃分,接下來我們就來看看幾個常見的記憶體回收演算法。當然,下方所介紹的記憶體回收的演算法不僅是JVM中所使用到的,我們也會回顧OC中的記憶體回收方式。下方主要包括「引用計數式回收」、「複製式回收」、「標記整理式回收」、「分代式回收」。

 

1、引用計數式記憶體回收

引用計數(

Reference Count)式記憶體回收機制是Objective-C以及Swift語言中正在使用的記憶體回收機制,在先前的部落格中我們也詳細的聊過引用計數式的記憶體回收。只要有引用,那麼引用計數就加1。當引用計數為0時,該區塊記憶體就會被回收。當然這中記憶體清理方式容易形成「引用循環」。

Objective-C的參考計數中循環引用而造成記憶體外洩的問題,可以將變數宣告為weak或strong型別。也就是說我們可以將引用定義為「強引用」或「弱引用」。當出現「強引用循環」時,我們將其中的一個引用設為weak類型即可,然後這種強引用循環就被打破了,也就不會造成「記憶體外洩」的問題。關於「引用計數式記憶體回收」的更多以及更詳細的內容,請參考先前發布的關於OC內容的相關部落格。

為了更清晰的了解引用計數的工作方式,就簡單的畫了下方這個圖。在左邊的堆疊中的a、b、c三個引用分別指向堆中的不同區域塊。在堆中的記憶體區域區塊中,該區域有一個強引用時,其retainCount就會加1。而在弱引用時,就retainCount不會加1。

我們先來看看a引用的第1塊記憶體區域,因為該記憶體區塊只有a在強引用,所以retainCount=1,當a不在引用該記憶體區域時,retainCount=0,該記憶體會理解被回收的。這種情況下是不會造成記憶體外洩的。

我們再來看看b指向的記憶體區域2。 b和記憶體區塊3都強烈引用了記憶體區塊2,所以2的retainCount=2。而記憶體區塊2也強引用了記憶體區塊3,所以3的retainCount=1。所以b指向的這塊記憶體區域就存在“強引用循環”,因為當b不再指向這塊記憶體區域時,rc=2就會變成rc=1。因為retainCount不為零,所以這2塊記憶體區域是不會被釋放的,2不會被釋放,那麼自然而然的3塊內存區域也不會被釋放,但是這塊內存區域有不會再被使用到了,所以就會造成「記憶體外洩」的情況。如果這兩塊記憶體區域特別大,那麼我們可想而知,後果是比較嚴重的。

像c引用的這塊情況,就不會引起“強引用循環”,因為其中的一個引用鍊是弱引用的。當c不在引用第4塊記憶體時,rc由1變為零,那麼該區塊區域就會立即釋放。而記憶體區塊4被釋放後,記憶體區塊5的rc由1變成0,記憶體區塊5也會被釋放掉。這種情況下是不會造成記憶體外洩的。而在Objective-C中正是採用的這種方式來回收記憶體的,當然了,在OC中除了「強引用」和「弱引用」外,還有自動釋放池。也就是說,Autorealease類型的引用,讓retainCount = 0時,不會被立即釋放掉,而是在出自動釋放池時才會被釋放掉,在此就不做過多贅述了。

  

 

#2、複製式記憶體回收

聊完引用計數回收,我們知道引用計數容易引起“循環引用”的問題,為了解決“循環引用”引起的內存洩漏問題,OC中引入和“強引用”和“弱引用”的概念。接下來我們在看看複製式記憶體回收機制,在該機制中是不需要關心「循環引用」的問題的。簡單的說,複製式回收其核心就是“複製”,但前提是有條件複製。在垃圾回收時,將「活對象」複製到另一塊空白的堆區,然後將先前的區域一併清除。 「活物件」就是指沿著物件的引用鏈可以到「棧」上的物件。當然將活體物件複製到新的「堆區」後,也要將棧區的參考進行修改。

下方就是我們畫的複製式回收的簡圖,主要將堆分為兩大部分,在進行垃圾回收時,會將一個堆上的活對象複製到另一個堆上。下方堆1區是目前正在使用的區塊,堆2區則是空閒區。而在堆1區中未被標記的那些記憶體區塊,也就是2、3是要被回收的垃圾物件。而1、4、5是要被複製的「活對象」。因為沿著堆疊上的a可到達區塊1、沿著c可到達區塊4、5。而區塊2和3雖然有引用,但是不是來自非堆區,也就是2和3的引用都是來自堆區的引用,所以是要被回收的物件。

  

找到了活物件後,接下來要做的就是將活物件複製,將其複製到堆2區。當然,複製到堆2區的物件間的記憶體位址是連續的,如果要分配新的記憶體空間的話,直接從堆空閒的一段分配即可。這樣在分配記憶體空間時的效率是比較高的。物件複製後,要修改來自「非堆區」的引用位址。如下圖所示。

  

複製完畢後,我們直接將堆2區的中的所有記憶體空間回收即可,下方就是複製回收後的最終結果。下方的堆1區清空後,可以接收複製過來的物件了。當堆2區進行垃圾回收時,會把堆2區的活物複製到堆1區上。

從這個實例中我們可以看出當記憶體垃圾特別多的時候「複製式」垃圾回收的效率還是比較高的,因為複製的物件比較少,清除時直接將舊的堆空間進行清理即可。但是,當垃圾比較少的時候,這種方式會複製大量的活對象,效率還是比較低的。這種方式也會將堆的儲存空間進行分半。也就是說,總有一半是空閒的,堆空間的利用率不高。

  

 

#3、標記-壓縮回收演算法

從上述##在「複製式」垃圾回收過程中,我們知道,垃圾多時其效率比較高,而垃圾少時,其工作方式效率是比較低的。那麼,接下來,我們來介紹另一種標記-壓縮回收演算法,這種演算法在垃圾少時的工作效率比較高,而垃圾多的情況下,工作效率反而不高,這就與「複製式」形成了互補。下方我們將對標記-壓縮回收演算法進行介紹。

標記-壓縮的第一部就是標記,需要將堆區中的「

活物件」進行標記。上面的內容我們已經聊了什麼是“活對象”,在此就不做過多贅述了。由「活對象」的特徵我們可以看出,下方的活對像是記憶體區域1和3,所以我們將其進行標記。

  

標記完成後,我們就開始進行壓縮了,將活物件壓縮到「堆疊區」的一段,然後將剩餘的部分進行清除。下方就是將1和3這兩個活對象進行了壓縮。壓縮後,將下方的空間進行Clean。也就是說Clean的部分,就可以分配新的物件了。

  

下方截圖是標記-壓縮清理後的狀態。標記-壓縮式垃圾回收可充分利用堆區的空間,當垃圾比較少時,這種處理方式效率還是比較高的,如果垃圾太多碎片化嚴重時,移動的“活對象”較多,效率比較低。這種方式可以與「複製式」結合使用,根據目前堆區的垃圾狀態來選擇哪種回收方式。剛好與

「複製式」形成優勢互補。將「複製式」、「標記-壓縮式」的回收方式進行整合的演算法,就是「分代式」垃圾回收機制,下方會詳細介紹。

  

 

#4、分代垃圾回收

#「分代」即根據物件易產生垃圾的狀態或物件的大小將其分為不同的代,可分為「年輕代」、「年老代」和「永久代」。 「永久代」不在堆中,再次先不做討論。根據分代垃圾回收的特點,畫了下方的簡圖。

在堆中,主要把區域分為「年輕代」、「年老代」。位於“年輕代”的物件記憶體創建的時間不長,更新比較快,易產生“內存垃圾”,所以“年輕代”的垃圾回收使用“複製式”回收方式效率比較高。 「年輕代」又可分為兩個區,一個是

Eden Space(伊甸園)和Survivor Sprace(倖存者區)。 Eden Space去主要存放那些初次被創建的對象,而Survivor Sprace存放的是從Eden Space倖存下來的「活對象」。在Survivor Sprace(倖存者區)中又分為form和to兩塊,用於相互複製物件來進行垃圾清理。

而“年老代”中存放的是一些“大對象”以及從

Survivor Sprace中存活下來的“對象”,一般到“年老代”的對像比較穩定,產生垃圾較少,針對這種情況,使用「標記-壓縮」式回收效率比較高。 「分代垃圾回收」主要是分而治之,根據不同對象的特性將其分類,根據分類的特點來具體選擇合適的垃圾回收方案。

  

 

#三、分代式垃圾回收的具體運作原理##

當然在JVM具體的垃圾回收時,根據線程分可分為使用單個線程回收的“串行垃圾回收”,使用多個線程回收的“並行垃圾回收」。依程式的掛起狀態,又可分為「獨佔式回收」與「並發式回收」。當然之前也多次聊過「並行」與「並發」絕對不是一個概念,切不可將其混淆。這篇部落格就不對上述這些方式進行詳述了,有興趣的,請自行Google。

下面我們來看看「分代式垃圾回收」的具體工作原理的完整步驟,來直觀的感受一下「分代式」的垃圾回收的執行方式。

 

1、垃圾回收前

#下圖是等待「分代垃圾回收 」的簡圖,從下圖中,我們可以看出在堆中有些已分配的物件記憶體並沒有被堆疊上引用,這些就是要被回收的物件。我們可以看出,下方的堆,整體上分為“年輕代”和“年老代”,而年輕代,有可細分為Eden Space, From以及To三個區域。關於每個區域的作用,在上面介紹「分代垃圾回收」時,我們已經介紹過了,所以在此部分我們不做詳細介紹了。

  

 

#2、分代垃圾回收

下圖是上述堆控件的垃圾回收過程。因為我們有上圖可以看出,To區域是空白區,可以接受被複製的物件。由於「年輕代」易產生記憶體垃圾,所以採用「複製式」記憶體回收的方式。我們將Eden Space和From兩個堆疊區塊中的「活物件」拷貝到To區。拷貝的同時,我們也要修改被拷貝記憶體的堆疊引用位址。而對From或Eden區域的「大物件」儲存空間則直接複製到「年老代」。因為「大物件」在From與To區多次複製的效率比較低,直接將其加入「年老代」以提高回收效率。

對於「年老代」的垃圾回收,就採用「標記-壓縮」式垃圾回收。首先,先將活對象進行「標記」。

  

 

#3、垃圾回收後的結果

下方就是「分代”垃圾回收後的具體結果。從下方簡圖中,我們可以看出,Eden Space和From中的活對像都被複製到了To區,而「年老代」的堆區的儲存空間也變化不少。而且在「年老代」中多出了從From區複製過來的大物件。具體如下圖所示。

  

 

 

四、Eclipse的GC日誌設定與分析

#上面聊這麼多,接下來我們來直觀的感受一下在Eclipse如何查看垃圾回收的過程以及分析垃圾回收的日誌資訊。預設情況下,是不顯示垃圾回收的過程以及列印日誌的,需要在運行配置中添加相關的配置項來列印垃圾回收的日誌。本部分我們來看看Eclipse中的垃圾回收日誌記錄的配置,然後我們來分析一下這些日誌記錄。當然我們這篇部落格中使用的是Java8,如果你用其他版本的Java列印出來的日誌資訊會略有不同,好開始本部分的內容。

1、設定Eclipse的運行設定

#在Eclipse中的運作設定中加入對應的配置項,垃圾回收時才會列印相應的日誌資訊。選擇我們的工程,然後找到

Run Configurations…

選項,進行運行時的設定。 ######  ######### ###

下方就是上述選項開啟的對話框,然後找到(x)=Arguments這個標籤欄,在VM arguments中加入對應的虛擬機器參數,這些參數都會作為工程在運作時的參數。下面我們新增了-XX:+PrintGCTimeStamps-XX:+PrintGCDetails兩個參數。由這兩個參數名稱我們不難看出對應參數所對應的功能,一個是列印垃圾回收時的時間戳,另一個是列印垃圾回收時的細節。當然還有好多其他的參數,例如選擇“垃圾回收”時的具體演算法的參數,以及選擇是“串行”還是“並行”的參數,還有一些選擇是“獨佔式”還是“並發式”垃圾回收的參數。在此就不做過多贅述了,請自行Google。

  

 

#2、回收日誌的列印與解析

##已配置完上述參數後,當我們使用

System.gc(); 來進行強制垃圾回收時,會列印出對應的參數資訊。首先我們得創建測試用的程式碼,下方就是我們所創建的測試類,當然測試類中的程式碼比較簡單。主要是new了以字串,然後將引用置為null, 最後呼叫System.gc()進行回收。具體程式碼如下所示:

package com.zeluli.gclog;public class GCLogTest {public static void main(String[] args) {
        String s = new String("Value");
        s = null;
        System.gc();
    }
}
 

下方就是上述程式碼所執行的效果,接下來我們將介紹下方日誌資訊的主要內容。

  • [PSYoungGen: 1997K->416K(38400K)] 1997K->424K(125952K), 0.0010277 secs]

    • PSYoungGen表示,並行對「年輕代」進行回收,1997K->416K表示年輕代相應區域中「回收前->回收後」的大小,而(38400K)表示「年輕代」堆的總大小。而後方的1997K->424K(125952K)資料是以整個堆的角度來看待的問題。 1997K(堆回收前使用的記憶體) -> 424K(堆回收後使用的記憶體)(125952K-堆的總記憶體空間)。

  • [ParOldGen: 8K->328K(87552K)]

    • ParOldGen並行回收“年老代”,後邊的參數與上述並行回收年輕代的參數類似,就不多說了。

  • [Metaspace: 2669K->2669K(1056768K)​​]

    • #則表示「元資料區」的回收情況,Metaspace及「永久代」區,用於存放靜態資料或系統方法的區域。

  

 

上述就是簡單的垃圾回收的日誌,這篇部落格的內容就先到這裡吧,關於JVM中的垃圾回收的內容還有很多,以後結合著具體情況,再陸陸續續的進行介紹。今天博客就先到這裡。

 

以上是JVM的記憶體區域劃分以及垃圾回收機制詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn