從萌新的角度理解JVM記憶體管理

塗印發表於2018-09-14

1. JVM記憶體管理機制

在進行Java程式設計時,一般不涉及記憶體的分配和記憶體回收的相關程式碼,此處引用一句話:Java和C++之間存在一堵由記憶體動態分配和垃圾收集技術所圍成的高牆,牆外的人想進去,牆裡面的人想出來,個人從這兩句話中,捕獲到了兩個點

  • java的自動記憶體管理機制,極大的節省了開發人員的精力,避免了易錯且複雜的記憶體管理設計,相對於手動的記憶體管理這是極大的飛躍。
  • java自動記憶體管理機制,其不能根據具體的場景提供最優的記憶體管理,其只提供普適的記憶體管理機制。想比於C++的手動記憶體管理,靈活性不夠,存在制約系統效能的瓶頸。

第二點是我們深入瞭解JVM記憶體管理機制的意義,通過對原理的把握,在指定的場景下設計JVM最優的記憶體管理策略,本文內容組織結構如下:

  • JVM記憶體分配
  • JVM記憶體回收

2. JVM記憶體分配

ClassExample refereenceExample  = new ClassExample ();
複製程式碼

上述程式碼,如果粗略劃分的話,可以劃分為兩個過程:

  1. 在堆區分配物件記憶體
  2. 將物件記憶體地址賦值給物件引用 上述兩個過程基本就是記憶體分配中,比較重要的兩個知識點了,記憶體分配策略和物件引用 #####2.1 堆區佈局 物件分配在堆上這是毫無疑問的,如果在往下細分的話,那麼堆區的記憶體佈局還是挺有講究的,大致可以分為如下佈局:
    堆記憶體佈局
  • 新生代:從名字可見一般,新生代區域中存放的物件一般是兩種:剛剛被new出來的物件,或者經歷記憶體回收次數不多的物件
  • 老年代:老年代從名字也可見一般,一般存放兩種物件: 佔用記憶體比較多的大物件和經歷過多次記憶體回收過程的物件
  • 永久代:永久代的記憶體就比較固定,每個類的class物件,一般存放在永久代當中。

上述JVM堆區中的記憶體佈局代表的是邏輯檢視,並不是實際的物理佈局,實際上,JVM了提供多種不同的記憶體分配和回收的策略,每種策略抽閒出邏輯檢視都會有細微的差別,但是上述邏輯檢視可以說是所有邏輯檢視的根檢視

2.2 記憶體分配一般過程

記憶體分配的一般過程
在圖中有幾個重要的概念,需要著重強調:

  • 物件大小閾值設定,在JVM中可以通過設PretenureSizeThreshold這個引數,指定該閾值大小,如果待分配物件大小超過該閾值,嘗試在老年代中開闢記憶體空間儲存物件,否則嘗試在新生代中開闢記憶體空間儲存物件
  • MinorGC,這是JVM中一次記憶體回收過程,記憶體回收過程又稱垃圾回收過程,這次記憶體回收是由新生代中記憶體空間不足引起的,主要對新生代中的記憶體物件進行回收
  • FullGC,這也是JVM中的一次記憶體回收過程,這次記憶體回收過程是有老年代中記憶體空間不足造成的,主要針對所有堆區中的記憶體物件進行回收,包括新生代,老年代,以及永久代
  • 物件年齡,經過一次記憶體回收後依然存活的物件,其年齡會加1。當物件年齡超過一個指定閾值後,其由新生代轉向老年代儲存。這個物件年齡的閾值,同樣可以通過設定JVM的MaxTenuringThreshold引數進行指定
2.3物件引用

JVM記憶體區域的佈局詳情中介紹了物件引用相關的內容

3. JVM記憶體回收

從程式設計師角度來看,記憶體回收的過程是透明,具體細節都對程式設計師遮蔽了。JVM記憶體區域的佈局詳情,仔細的介紹了JVM中的記憶體模型以及各個記憶體區儲存的資料型別。

JVM記憶體佈局
記憶體回收主要針對的記憶體區域主要是堆區和方法區,在上文中談及了MinorGC以及FullGC。MinorGC主要是針對堆區進行記憶體回收,FullGC除了對堆區進行回收,對方法區也進行回收,是相對重量級的回收動作。在記憶體回收這個章節中,主要從以下三個方面闡述: 1. 什麼樣的物件可回收? 解決方法:可達性分析 2. 如何回收記憶體? 解決方法:標記-清除演算法,標記-複製演算法,標記-整理演算法等記憶體回收演算法 3.什麼時候回收合適? 解決方法:安全點和安全區域

3.1 可達性分析

可達性分析示意圖
在上圖中,一共有七個物件,箭頭的走向代表引用關係、物件1引用物件2,物件2引用物件3。物件1是根節點(GCROOT),其他的是非根節點(GCROOT)。 **可達性分析是指:不能被GCROOT節點通過引用鏈達到的節點,將被列入擬回收物件範圍。物件6和物件7就不能被任何一個GCRoot節點所引用,是目標回收物件。**JVM認為這些物件不具備使用價值,可以將其進行記憶體回收。 **根節點(GCROOT)**是上文中提出的重要概念,一般來說,如下物件可作為根節點物件: (1)JVM虛擬機器棧中引用的物件 (2)方法區中類靜態屬性引用物件 (3)方法區中類常量引用物件 (4)本機方法棧中引用物件 可達性分析是進行記憶體回收的判定條件,在可達性分析之後,確定哪些物件是回收目標。除了可達性分析之外,引用計數法同樣可用來進行判斷記憶體回收目標物件,但是其無法解決迴圈引用問題。主流的JVM都採用可達性分析。

3.2記憶體回收演算法

通過可達性分析之後,將確定哪些物件是回收目標,接著記憶體回收演算法將執行具體的回收細節。下圖是記憶體區域中三個狀態,空閒記憶體是未使用記憶體,目標回收記憶體是可達性分析之後可回收記憶體,已佔記憶體是不需要回收記憶體。

記憶體物件

  • 標記-清除演算法 標記-清除演算法是比較直白的記憶體回收演算法,其直接釋放目標回收記憶體,沒有其他任何附加操作。該演算法執行後的結果如下:
    標記-清除演算法執行後的記憶體圖
    標記-清除演算法,邏輯簡單,易於理解,但是其有個致命的缺點就是其極易產生記憶體碎片,在分配大物件時,不能有足夠連續的記憶體空間而造成頻繁的GC。
  • 標記-複製演算法 標記-複製演算法是針對記憶體碎片問題提出的一種演算法。
    image.png
    在標記-複製演算法的設定中,始終有塊空白記憶體區域,如上圖長記憶體區域二。在記憶體回收時,將記憶體區域一種存活物件全部按序複製到記憶體區域二中後再對記憶體區域一中所有物件進行回收。通過這種做法可以極大的避免記憶體碎片,但是空白記憶體區域將始終閒置,記憶體利用率不高。在上文中提及將堆區的記憶體分為兩個surivior區以及Eden區就是採用標記-複製演算法。Eden區和surivior之間大小比例分配預設是8:1:1.
  • 標記-整理演算法 標記-整理演算法,同樣是為解決記憶體碎片提出的記憶體回收演算法,其在標記-清楚演算法的基礎上增加的整理功能,對記憶體物件進行拷貝移動,以保證空間記憶體空間連續。
    標記-整理演算法回收後記憶體
    標記-整理演算法,能夠克服記憶體碎皮,且不需要額外記憶體,但是其拷貝和移動物件的時間消耗較高。該演算法常用在老年代物件回收中。
3.3 什麼時候回收記憶體合適

通過可達性分析,可知哪些物件是需要回收的物件,在這過程中需要列舉根節點,這是一個耗時操作,為了保證記憶體回收的順利進行,必須保證引用關係的一致性,即在記憶體回收過程中物件引用關係不會發生變化。只有在這些地方進行記憶體回收才是安全點,因此JVM引入了安全點的概念,安全點處的物件引用關係不會改變,適合記憶體回收。安全區域是對安全點概念的擴充,指在這一段區域內物件引用關係具有一致性,能進行記憶體回收。

相關文章