先打好基礎,阿里架構師一文帶你深刻理解JVM內存回收機制

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等java

1、垃圾回收發生的區域

堆是java建立對象的區域(String對象在常量池中),也是垃圾回收最多的地方。可是除了堆空間還有方法區存在須要回收的垃圾git

回收方法區github

廢棄的常量面試

在常量池中存在一個字面量A,若是系統中沒有一個地方引用`A``,這時候發生垃圾回收,若是有必要這個字面量就會被清理出常量池。算法

注意是若是有必要。好比上一篇文章中引用的例子,就沒有回收字符串。安全

無用的類數據結構

當知足如下條件時,這個類就能夠被回收,而不是必定會回收。ide

  1. 全部的類實例都已經被回收也就是java堆裏面不存在該類的任何實例
  2. 加載類的ClassLoader已經被回收
  3. 該類對應的Java.long.Class對象任何地方被引用,沒法經過反射訪問該類的方法。

2、如何判斷對象是否能夠被回收

java有一個很是大的好處就是會自動進行垃圾回收,而不用手動釋放對象所佔用的內存。當以一個對象再也不被引用的時候就能夠進行垃圾回收,那麼如何判斷一個對象是否在被使用呢?學習

引用計數法線程

引用計數法很簡單,只須要在對象建立之初給對象加一個引用計數器,每當有一個地方引用他就+1,引用失效就-1,當引用計數器爲0,則對象再也不被引用。每次垃圾回收,只須要遍歷一遍全部的引用計數器就能夠。可是對於循環引用,引用計數法則沒法釋這兩個對象。

可達性分析算法

經過一系列被稱爲GC Root的對象爲起點,從這些節點往下搜索,搜索走過的路徑稱之爲引用鏈,當一個對象到GC Root沒有任何引用鏈的時候,則證實此對象不可達。

圖1  可達性分析示例圖

在JVM中,能夠被用做GC Root的對象有:

  • 虛擬機棧中引用的對象
  • 方法區中靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中引用的對象

3、HotSpot實現

枚舉根節點

對於根節點的枚舉有以下的問題

  1. 能夠做爲根節點(GC Roots)的節點主要是全局性的引用(方法去中靜態屬性引用的對象和方法區中常量引用的對象)與執行上下文(棧中引用的對象)
  2. 在一次可達性分析過程當中,不能出現分析過程當中對象引用關係還在不斷變化的狀況,不然沒法保證分析結果的準確性,爲了達到這一目的,GC過程當中就必須停頓全部的java線程
  3. 垃圾收集時,手機線程會對棧上的內存進行掃描,看看哪些位置存儲了Reference類型,若是發現某個位置確實存的是Reference類型,整個Reference所引用的對象就能夠做爲根節點,
    他所能到達的對象都不能被回收。
  4. 棧上的本地變量表中只有一部分是Reference類型,而那些非Reference類型的數據對於垃圾回收毫無用處,可是若是對於棧進行全棧掃描將會是一種對時間和資源的浪費,尤爲是暫停了用戶線程

解決方法

是否能夠用額外的空間記錄下每一個Reference的位置,這樣的話GC的時候從這個結構中直接讀取這個結構,而不用進行全棧掃描。事實上,大部分主流的虛擬機也確實是這樣作的,以HotSpot爲例,它使用一種OopMap的數據結構來保存這類信息。

一個棧意味着一個線程,而一個棧楨表明了一個方法,每一個被JIT編譯事後的方法會在一些特定的位置記錄下OopMap記錄了執行到該方法的某條指令的時候,棧上和寄存器的哪些位置是引用,這樣GC在掃描到這些棧的時候就會查詢這些OopMap就知道哪裏是引用。這些位置主要在:

  • 循環的末尾
  • 方法臨返回前/調用方法的call指令以後
  • 可能拋出異常的位置

而這些位置就被稱之爲「安全點」,之因此要選擇一些特定位置來記錄OopMap,是由於若是對每條指令的位置都記錄OopMap的話,這些記錄就會比較大,那麼空間開銷就會顯得不值得。

GC發生時,程序首先運行到最近的一個安全點停下來,而後更新本身的OopMap,枚舉根節點時,遞歸遍歷每一個棧楨的OopMap,經過棧中記錄的被引用的對象的內存地址,便可找到這些對象。

安全點與安全區域

安全點

程序在執行時並非任什麼時候間均可以進行GC,只有到達有OopMap記錄的位置才能夠執行GC,整個位置稱之爲安全點

安全點的選定基本是以程序「是否具備讓程序長時間執行的特徵」爲標準選定的。程序通常不會由於指令流太長而長時間執行(每一個指令執行的時間都很短)。「長時間執行」的典型特徵就是指令序列的服用,例如:循環、遞歸、方法調用。因此具備這些功能的指令纔會產生安全點。

安全區域

安全區域指在這一段代碼之中,引用關係不會發生變化,在這一段代碼之中,任一點都是安全點。任何一個地方均可以中斷線程開始GC。

當線程執行到安全區域後,首先標識本身已經進入安全區域,那麼這段時間JVM要發起GC時就不用管標記本身進入安全區的線程。線程要離開安全區時,首先須要先檢查系統是否已經完成了根節點的選舉,若是完成則線程繼續執行,不然要繼續等待收到能夠安全離開安全區的信號。

如何保證GC發生時,全部的線程都跑到了安全點上呢?

當要進行GC的時候,會讓全部的線程都在安全點中斷,就有兩種方式:

  • 搶佔式中斷:不須要代碼配合。當GC發生時,讓全部的線程都終端,而後讓不在安全點的線程繼續執行到安全點上。不過通常不採用這種方式
  • 主動式中斷:當GC須要中斷線程時,不對線程進行操做,僅設置一個標識。各個線程輪詢這個標識,當發現這個標識被設置時,使得程序運行到最進的安全點時,主動掛起。

標識的設置和安全點是重合的,標識的設置和安全點是重合的。除此以外還有一個建立對象須要分配內存的地方。

4、垃圾回收算法

假設存在以下的內存區域:

圖2  原始狀況內存中對象的分佈

下文將以這塊內存爲例進行垃圾收集算法的分析

標記-清除算法

顧名思義,標記清除算法會爲兩個階段,1-標記,2-清除。

標記:垃圾收集器從GC Roots出發,進行搜索,而後對全部能夠訪問的對象打上標識,標記其爲可達的對象,標記通常保存在header中

圖3  標記階段

清除:垃圾收集器對堆內存進行線性遍歷,若是發現某個對象沒有被標記爲可達,就會將其回收,回收後效果以下圖

圖4  標記清除算法進行垃圾回收

優勢

  1. 實現簡單
  2. 與保守式GC算法兼容

缺點

  1. 內存碎片化嚴重
  2. 分配速度緩慢,因爲空閒塊的維護是用鏈表實現的,分塊可能不連續,每次分配都須要遍歷鏈表,極端狀況下要遍歷震整個鏈表。
  3. 標記和清除的效率都不高,

複製算法

複製算法,就是將內存劃分爲相等的兩塊,每次只是用其中一塊,當這塊內存使用完了就將還存活的對象複製到另外一塊,而後將這塊空間清理掉,這樣使得每次對內存的回收都是半區回收。

複製算法的示意圖以下圖:

圖5  複製算法

優勢

  1. 內存分配時不用考慮碎片的狀況只須要移動棧頂指針分配內存便可
  2. 實現簡單,高效

缺點

  1. 可用內存縮小爲原來的一半

標記—整理算法

複製算法在對象存活較多的時候會進行較多的操做,若是對象所有存活複製將會進行100%,而且浪費50%的內存空間做爲擔保。

標記—整理算法和標記—清除算法前半部分同樣,只是後續不是清理,而是讓全部存活的對象都向一端移動,而後清理掉邊界之外的內存。

圖6  標記整理算法

5、JVM中使用的垃圾收集算法

在當前主流的垃圾收集器當中(g1除外),基本都採用一種分代收集算法。根據對象存活週期,將java堆分爲新生堆和老年堆。對於新生堆,採用複製算法,對於老年堆採用標記-清除或者標記-整理算法。

研究人員發現大多數的對象都是「朝生夕滅」,對於這樣的對象,生存週期很短,能夠將其放入新生堆,由於其生存時間很短,因此新生堆採用複製算法的時候沒有必要使用1:1的比例劃份內存。

而是分爲較大的Eden空間和兩塊較小的Suvivor空間;HotSpot的Eden和Suvivor的比例爲8:1。回收時將Eden和一塊Suvivor上還存活的對象,一次性copy到另外一塊Suvivor上,而後清理掉之前的兩塊區域。這樣每次新生代可用的內存空間佔整個新生堆的90%,只有10%會被浪費。

咱們沒有辦法保證新生代回收的時候只剩下很少於10%的對象存活。當Suvivor空間不夠用時,就須要依賴其餘內存(老年堆)進行分配擔保。對於存活過必定gc次數的對象放進老年堆。

老年堆對象存活率高,使用複製算法可能就須要1:1的空間,這樣就會浪費內存,所以使用的是標記-清除或者標記-整理算法。

6、GC的分類

保守式GC

HotSpot虛擬機在棧上使用OopMap記錄下了哪些位置是引用類型,根據記錄的類型類型開始查找堆中存活的對象。

虛擬機最初的實現當中是沒有記錄每一個數據的類型的,JVM也沒法區份內存裏某個位置的數據到底應該解讀爲引用類型仍是其餘數據類型,這種條件下,實現出來的GC就是「保守式GC」。在進行GC時,JVM開始從一些已知的位置(例如棧)開始掃描內存,掃描的時候每看到一個數字就看看它「像不像是一個指向GC堆中的指針」。

這裏會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(一般分配空間的時候會有對齊要求,假如說是4字節對齊,那麼不能被4整除的數字就確定不是指針),之類的。而後遞歸的這麼掃描出去。

優勢

  1. 實現簡單

缺點

  1. 會有部分對象原本應該已經死了,但有疑似指針指向它們,使它們逃過GC的收集。會有一部分已經不須要的數據佔用着GC堆空間,可是全部應該存活的對象都會活着,對程序語義來講時安全的
  2. 因爲是疑似指針,那麼就不知道這個究竟是不是指針,因此這些值就都不能改寫。移動對象就須要改寫指針,也就是說對象不可移動,所以通常使用標記-清除的方式來進行垃圾回收。

有一種辦法能夠在使用保守式GC的同時支持對象的移動,那就是增長一個間接層,不直接經過指針來實現引用,而是添加一層「句柄」(handle)在中間,全部引用先指到一個句柄表裏,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表裏的內容便可。

半保守式GC

保守式GC沒有在JVM中記錄任何類型信息,半保守式GC會在對象上記錄類型信息,這樣的話,掃描棧的時候仍然和保守式GC同樣,可是掃描到堆上的時候,對象上帶了足夠的類型信息,JVM就能判斷出棧中這個位置是否是一個指向堆中對象的指針,以及這個對象內什麼位置數據是引用類型,這種是「半保守式GC」,也稱之爲「根上保守」。

因爲半保守式GC在堆內部的數據是準確的,因此它能夠在直接使用指針來實現引用的條件下支持部分對象的移動,方法是隻將保守掃描能直接掃到的對象設置爲不可移動(pinned),而從它們出發再掃描到的對象就能夠移動了。

準確式GC

對於垃圾回收,JVM關心的就是掃描的根節點是否是一個指向堆內存的指針,那麼就是在棧上記錄下那個位置式引用類型,是指向堆上對象的指針,在HotSpot虛擬機中這個數據結構就是OopMap

總結

  1. 垃圾回收不止是發生在堆區,對於方法區中產生的垃圾有可能會被回收。在以前的從JDK源碼理解java引用一文中舉了不會被回收的例子
  2. 虛擬機通常採用引用可達性分析算法來尋找不被使用的對象,其實尋找到的是正在被使用的對象,剩下的就是再也不被使用的對象。
  3. 除了g1垃圾收集器。其餘的垃圾收集器都有明顯的區分老年代和新生代進行垃圾回收,因爲老年代和新生代對象存貨時間不同,採用不一樣的垃圾回收算法
相關文章
相關標籤/搜索