JVM的內存區域劃分以及垃圾回收機制詳解

在咱們寫Java代碼時,大部分狀況下是不用關心你New的對象是否被釋放掉,或者何時被釋放掉。由於JVM中有垃圾自動回收機制。在以前的博客中咱們聊過Objective-C中的MRC(手動引用計數)以及ARC(自動引用計數)的內存管理方式,下方會對其進行回顧。而目前的JVM的內存回收機制則不是使用的引用計數,而是主要使用的「複製式回收」和「自適應回收」。算法

固然除了上面是這兩種算法外,還有其餘是算法,下方也將會對其進行介紹。本篇博客,咱們先簡單聊一下JVM的區域劃分,而後在此基礎上介紹一下JVM的垃圾回收機制。緩存

 

1、JVM內存區域劃分簡述併發

固然本部分簡單的聊一下JVM的內存區域的劃分,爲下方垃圾回收機制內容的展開進行鋪墊。固然對JVM內存區域劃分的內容網上有好多詳細的內容,請自行Google。測試

根據JVM內存區域的劃分,簡單的畫了下方的這個示意圖。區域主要分爲兩大塊,一塊是堆區(Heap),咱們所New出的對象都會在堆區進行分配,在C語言中的malloc所分配的方法就是從Heap區獲取的。而垃圾回收器主要是對堆區的內存進行回收的。spa

而另外一部分則是非堆區,非堆區主要包括用於編譯和保存本地代碼的「代碼緩存區(Code Cache)」、保存JVM本身的靜態數據的「永生代(Perm Gen)」、存放方法參數局部變量等引用以及記錄方法調用順序的「Java虛擬機棧(JVM Stack)」和「本地方法棧(Local Method Stack)」。線程

  

垃圾回收器主要回收的是堆區中未使用的內存區域,並對相應的區域進行整理。在堆區中,又根據對象內存的存活時間或者對象大小,分爲「年輕代」和「年老代」。「年輕代」中的對象是不穩定的易產生垃圾,而「年老代」中的對象比較穩定,不易產生垃圾。之因此將其分開,是分而治之,根據不一樣區域的內存塊的特色,採起不一樣的內存回收算法,從而提升堆區的垃圾回收的效率。下方會給出具體的介紹。3d

 

 

2、常見的內存回收算法簡介日誌

上面咱們簡單的瞭解的JVM中內存區域的劃分,接下來咱們就來看一下幾種常見的內存回收算法。固然,下方所介紹的內存回收的算法不只僅是JVM中所使用到的,咱們還會回顧一下OC中的內存回收方式。下方主要包括「引用計數式回收」、「複製式回收」、「標記整理式回收」、「分代式回收」。code

 

一、引用計數式內存回收orm

引用計數(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時,不會被當即釋放掉,而是在出自動釋放池時纔會被釋放掉,在此就不作過多贅述了。

  

 

二、複製式內存回收

聊完引用計數回收,咱們知道引用計數容易引發「循環引用」的問題,爲了解決「循環引用」引發的內存泄露問題,OC中引入和「強引用」和「弱引用」的概念。接下來咱們在看看複製式內存回收機制,在該機制中是不須要關心「循環引用」的問題的。簡單的說,複製式回收其核心就是「複製」,但前提是有條件複製。在垃圾回收時,將「活對象」複製到另外一塊空白的堆區,而後將以前的區域一併清除。「活對象」就是指沿着對象的引用鏈能夠到「棧」上的對象。固然在將活對象複製到新的「堆區」後,也要將棧區的引用進行修改。

下方就是咱們畫的複製式回收的簡圖,主要將堆分爲兩大部分,在進行垃圾回收時,會將一個堆上的活對象複製到另外一個堆上。下方堆1區是目前正在使用的區塊,堆2區則是空閒區。而在堆1區中未被標記的那些內存塊,也就是二、3是要被回收的垃圾對象。而一、四、5是要被複制的「活對象」。由於沿着棧上的a可到達區塊一、沿着c可到達區塊四、5。而區塊2和3雖然有引用,可是不是來自非堆區,也就是2和3的引用都是來自堆區的引用,因此是要被回收的對象。

  

找到了活對象後,接下來要作的就是將活對象進行復制,將其複製到堆2區。固然,複製到堆2區的對象間的內存地址是連續的,若是要分配新的內存空間的話,直接從堆空閒的一段分配便可。這樣在分配內存空間時的效率是比較高的。對象複製後,要修改來自「非堆區」的引用地址。以下所示。

  

複製完畢後,咱們直接將堆2區的中的全部內存空間進行回收便可,下方就是複製回收後的最終結果。下方的堆1區清空後,能夠接收復制過來的對象了。當對堆2區進行垃圾回收時,會把堆2區的活對象拷貝到堆1區上。

從該實例中咱們能夠看出當內存垃圾特別多的時候「複製式」垃圾回收的效率仍是比較高的,由於複製的對象比較少,清除時直接將舊的堆空間進行清理便可。可是,當垃圾比較少的時候,這種方式會複製大量的活對象,效率仍是比較低的。這種方式也會將堆的存儲空間進行分半。也就是說,總有一半是空閒的,堆空間的利用率不高。

  

 

三、標記-壓縮回收算法

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

標記-壓縮的第一部就是標記,須要將堆區中的「活對象」進行標記。上面的內容咱們已經聊了什麼是「活對象」,在此就不作過多贅述了。由「活對象」的特徵咱們能夠看出,下方的活對象是內存區域1和3,因此咱們將其進行標記。

  

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

  

下方截圖是標記-壓縮清理後的狀態。標記-壓縮式垃圾回收可充分利用堆區的空間,當垃圾比較少時,這種處理方式效率仍是比較高的,若是垃圾太多碎片化嚴重時,移動的「活對象」較多,效率比較低。這種方式能夠與「複製式」結合使用,根據當前堆區的垃圾狀態來選擇哪一種回收方式。正好與「複製式」造成優點互補。將「複製式」、「標記-壓縮式」的回收方式進行整合的算法,就是「分代式」垃圾回收機制,下方會詳細介紹到。

  

 

四、分代式垃圾回收

「分代」即根據對象易產生垃圾的狀態或者對象的大小將其分爲不一樣的代,可分爲「年輕代」、「年老代」和「永久代」。「永久代」不在堆中,再次先不作討論。根據分代垃圾回收的特色,畫了下方的簡圖。

在堆中,主要把區域分爲「年輕代」、「年老代」。位於「年輕代」的對象內存建立的時間不長,更新比較快,易產生「內存垃圾」,因此「年輕代」的垃圾回收使用「複製式」回收方式效率比較高。「年輕代」又可分爲兩個區,一個是Eden Space(伊甸園)和Survivor Sprace(倖存者區)。Eden Space去主要存放那些初次被建立的對象,而Survivor Sprace存放的是從Eden Space倖存下來的「活對象」。在Survivor Sprace(倖存者區)中又分爲form和to兩塊,用於相互複製對象來進行垃圾清理。

而「年老代」中存放的是一些「大對象」以及從Survivor Sprace中存活下來的「對象」,通常到「年老代」的對象比較穩定,產生垃圾較少,針對這種狀況,使用「標記-壓縮」式回收效率比較高。「分代垃圾回收」主要是分而治之,根據不一樣對象的特色將其分類,根據分類的特色來具體選擇合適的垃圾回收方案。 

  

 

3、分代式垃圾回收的具體工做原理

固然在JVM具體的垃圾回收時,根據線程分可分爲使用單個線程回收的「串行垃圾回收」,使用多個線程回收的「並行垃圾回收」。根據程序的掛起狀態,又可分爲「獨佔式回收」和「併發式回收」。固然以前也屢次聊過「並行」與「併發」絕對不是一個概念,切不可將其混淆。本篇博客就不對上述這些方式進行詳述了,感興趣的,請自行Google。

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

 

一、垃圾回收前

下圖是等待「分代垃圾回收」的簡圖,從下圖中,咱們能夠看出在堆中有些已分配的對象內存並無被棧上引用,這些就是要被回收的對象。咱們能夠看出,下方的堆,總體上分爲「年輕代」和「年老代」,而年輕代,有可細分爲Eden Space, From以及To三個區域。關於每一個區域的做用,在上面介紹「分代垃圾回收」時,咱們已經介紹過了,因此在此部分咱們不作詳細介紹了。

  

 

二、分代垃圾回收

下圖是對上述堆控件的垃圾回收過程。由於咱們有上圖能夠看出,To區域是空白區,能夠接受被複制的對象。因爲「年輕代」易產生內存垃圾,因此採用「複製式」內存回收的方式。咱們將Eden Space和From兩個堆區塊中的「活對象」拷貝到To區。拷貝的同時,咱們也要修改被拷貝內存的棧引用地址。而對From或者Eden區域的「大對象」存儲空間直接將其複製到「年老代」。由於「大對象」在From與To區屢次複製的效率比較低,直接將其加入到「年老代」中以提升回收效率。

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

  

 

三、垃圾回收後的結果

下方就是「分代」垃圾回收後的具體結果。從下方簡圖中,咱們能夠看出,Eden Space和From中的活對象都被複制到了To區,而「年老代」的堆區的存儲空間也變化很多。並且在「年老代」中多出了從From區複製過來的大對象。具體以下所示。

  

 

 

4、Eclipse的GC日誌配置與分析

上面聊這麼多,接下來咱們來直觀的感覺一下在Eclipse如何查看垃圾回收的過程以及分析垃圾回收的日誌信息。默認狀況下,是不顯示垃圾回收的過程以及打印日誌的,須要在運行配置中添加相關的配置項來將垃圾回收的日誌進行打印。本部分咱們來看一下Eclipse中的垃圾回收日誌記錄的配置,而後咱們來分析一下這些日誌記錄。固然咱們本篇博客中使用的是Java8,若是你用其餘版本的Java打印出來的日誌信息會略有不一樣,好開始本部分的內容。

一、配置Eclipse的運行設置

在Eclipse中的運行設置中添加相應的配置項,垃圾回收時纔會打印相應的日誌信息。選擇咱們的工程,而後找到Run Configurations…選項,進行運行時的配置。

  

 

下方就是上述選項打開的對話框,而後找到(x)=Arguments這個標籤欄,在VM arguments中添加相應的虛擬機參數,這些參數都會做爲工程在運行時的參數。下方咱們添加了-XX:+PrintGCTimeStamps-XX:+PrintGCDetails兩個參數。由這兩個參數名咱們不難看出相應參數所對應的功能,一個是打印垃圾回收時的時間戳,另外一個是打印垃圾回收時的細節。固然還有好多其餘的參數,好比選擇「垃圾回收」時的具體算法的參數,以及選擇是「串行」仍是「並行」的參數,還有一些選擇是「獨佔式」仍是「併發式」垃圾回收的參數。在此就不作過多贅述了,請自行Google。

  

 

二、回收日誌的打印與解析

配置完上述參數後,當咱們使用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中的垃圾回收的內容還有好多,之後結合着具體狀況,再陸陸續續的進行介紹。今天博客就先到這兒。

相關文章
相關標籤/搜索