深入理解JVM內存回收機制(不包含垃圾收集器)


目錄

  • 垃圾回收發生的區域
  • 如何判斷對象是否可以被回收
  • HotSpot實現
  • 垃圾回收算法
  • JVM中使用的垃圾收集算法
  • GC的分類
  • 總結
  • 參考資料

垃圾回收發生的區域

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

回收方法區

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

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

無用的類
當滿足以下條件時,這個類就可以被回收,而不是一定會回收。

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

如何判斷對象是否可以被回收

java有一個非常大的好處就是會自動進行垃圾回收,而不用手動釋放對象所占用的內存。當以一個對象不再被引用的時候就可以進行垃圾回收,那么如何判斷一個對象是否在被使用呢?

引用計數法

引用計數法很簡單,只需要在對象創建之初給對象加一個引用計數器,每當有一個地方引用他就+1,引用失效就-1,當引用計數器為0,則對象不再被引用。每次垃圾回收,
只需要遍歷一遍所有的引用計數器就可以。但是對於循環引用,引用計數法則無法釋這兩個對象。

可達性分析算法

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


圖1  可達性分析示例圖

JVM中,可以被用作GC Root的對象有:

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

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需要中斷線程時,不對線程進行操作,僅設置一個標識。各個線程輪詢這個標識,當發現這個標識被設置時,使得程序運行到最進的安全點時,主動掛起。
    標識的設置和安全點是重合的,標識的設置和安全點是重合的。除此之外還有一個創建對象需要分配內存的地方。

垃圾回收算法

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


圖2  原始情況內存中對象的分布
下文將以這塊內存為例進行垃圾收集算法的分析

標記-清除算法

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

  1. 標記:垃圾收集器從GC Roots出發,進行搜索,然后對所有可以訪問的對象打上標識,標記其為可達的對象,標記一般保存在header中

圖3  標記階段
  1. 清除:垃圾收集器對堆內存進行線性遍歷,如果發現某個對象沒有被標記為可達,就會將其回收,回收后效果如下圖

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

優點

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

缺點

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

復制算法

復制算法,就是將內存划分為相等的兩塊,每次只是用其中一塊,當這塊內存使用完了就將還存活的對象復制到另一塊,然后將這塊空間清理掉,這樣使得每次對內存的回收都是半區回收。
復制算法的示意圖如下圖:


圖5  復制算法

優點

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

缺點

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

標記—整理算法

復制算法在對象存活較多的時候會進行較多的操作,如果對象全部存活復制將會進行100%,並且浪費50%的內存空間作為擔保。

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


圖6  標記整理算法

JVM中使用的垃圾收集算法

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

研究人員發現大多數的對象都是“朝生夕滅”,對於這樣的對象,生存周期很短,可以將其放入新生堆,因為其生存時間很短,所以新生堆采用復制算法的時候沒有必要使用1:1的比例划分內存。
而是分為較大的Eden空間和兩塊較小的Suvivor空間;HotSpotEdenSuvivor的比例為8:1。回收時將Eden和一塊Suvivor上還存活的對象,一次性copy到另一塊Suvivor
上,然后清理掉以前的兩塊區域。這樣每次新生代可用的內存空間占整個新生堆的90%,只有10%會被浪費。

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

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

GC的分類

保守式GC

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

虛擬機最初的實現當中是沒有記錄每個數據的類型的,JVM也無法區分內存里某個位置的數據到底應該解讀為引用類型還是其他數據類型,這種條件下,實現出來的GC
就是“保守式GC”。在進行GC時,JVM開始從一些已知的位置(例如棧)開始掃描內存,掃描的時候每看到一個數字就看看它“像不像是一個指向GC堆中的指針”。
這里會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(通常分配空間的時候會有對齊要求,假如說是4字節對齊,那么不能被4整除的數字就肯定不是指針),之類的。然后遞歸的這么掃描出去。

優點

  1. 實現簡單
    缺點
  2. 會有部分對象本來應該已經死了,但有疑似指針指向它們,使它們逃過GC的收集。會有一部分已經不需要的數據占用着GC堆空間,但是所有應該存活的對象都會活着,對程序語義來說時安全的
  3. 由於是疑似指針,那么就不知道這個到底是不是指針,所以這些值就都不能改寫。移動對象就需要改寫指針,也就是說對象不可移動,因此一般使用標記-清除的方式來進行垃圾回收。
    有一種辦法可以在使用保守式GC的同時支持對象的移動,那就是增加一個間接層,不直接通過指針來實現引用,而是添加一層“句柄”(handle)在中間,所有引用先指到一個句柄表里,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表里的內容即可。

半保守式GC

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

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

准確式GC

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

總結

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

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM