Java內存分配與回收機制

這篇文章主要講Java內存的分配與回收機制,主要包括Java運行時的數據區域

對象的創建垃圾收集算法回收策略

一.運行時數據區域

下圖是Java虛擬機運行時的內存示意圖:

Java內存分配與回收機制

從圖中我們可以看到Java內存總共分為6個部分:

  1. 程序計數器:每條線程都有一個獨立的程序計數器計數器可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時,就是通過改變這個計數器的值來選取下一條所需執行的字節碼指令、分支、循環、跳轉、異常處理,線程恢復等基礎功能都需要依賴這個計數器完成。
  • Java虛擬機棧:虛擬機棧是線程私有的,生命週期與線程相同。虛擬機棧為Java方法執行描述內存模型,每個方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應一個棧幀在虛擬機棧中入棧到出棧的過程。
  • 本地方法棧:與虛擬機棧發揮的作用相似。區別是虛擬機棧為執行Java方法服務,本地方法棧為Native方法服務
  • 堆:所有線程共享的區域。在虛擬機啟動時創建,所有的對象實例幾乎都在堆上分配。Java堆還可以細分為:新生代和老年代,再細緻一點有Eden空間、From Survivor空間、To Survivor空間。不過無論如何劃分,存儲的都是對象實例,進一步劃分的目的是為了更好的回收內存,或者更快的分配內存。
  • 方法區:方法區是各個線程共享的內存區域,主要用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。這塊區域與Java堆一樣不需要連續的內存和可以選擇固定大小或可擴展外,還可以選擇不實現垃圾收集。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,垃圾收集行為在這個區域較少出現。
  • 運行時常量池:運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面符和符號引用,這部分內容在類加載後進入方法區的運行時常量池中存放。
  • 直接內存:直接內存也稱堆外內存,它不是虛擬機運行時數據區的一部分。JDK1.4後引入NIO類,是一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接在堆外分配內存,然後通過存儲在Java堆中的DirectByteBuffer對象作為引用對這塊內存進行操作。這樣能夠顯著提高性能,避免Java堆和Native堆中來回複製數據。

所以通過表格的形式概括如下:

數據區域 概括 線程共享 程序計數器 當前線程所執行的字節碼的行號指示器 否 虛擬機棧 為Java方法執行創建棧幀存儲局部變量、操作數棧、動態鏈接、方法出口等信息 否 本地方法棧 與虛擬機棧類似,為Native方法服務 否 堆 存放對象實例 是 方法區 存儲虛擬機已加載的類信息、常量、靜態變量、即時編譯後的代碼等數據 是 運行時常量池 方法區的一部分,存放編譯期生成的字面量和符號引用 是 直接內存 被分配在堆外的內存,性能高,不受Java堆的大小限制 是 二.對象的創建與內存佈局

1.對象的創建

Java內存分配與回收機制

Java對象的創建

上圖是對象創建的完整流程圖,接下來做詳細說明。

  1. 當虛擬機收到new指令後,檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用所代表的類是否已被加載、解析和初始化過。如果沒有,必須先執行類加載過程。
  • 在類加載完成後可以確定對象分配所需要的空間。如果Java堆中內存是絕對規整的,用過的內存放一邊,空閒的內存放另一邊,中間放著一個指針作為分界點的指示器,那分配內存就只是把指針向空閒空間方向挪動一段與對象大小相等的距離,這種分配方式稱為"指針碰撞"。如果Java堆中內存不是規整的,空閒內存與使用過的內存是相互交錯的,虛擬機必須維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找出足夠的空間分配給對象實例,並更新列表上的記錄,這種分配方式稱為"空閒列表
    "。採用哪種分配方式通常由虛擬機的垃圾收集器是否帶有壓縮整理功能決定。
  • 劃分可用空間時,還需考慮為對象實例分配空間時是否是線程安全的。要保證線程安全,有兩種方案。一種是對分配內存空間的動作進行同步處理,實際上虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性。另一種是把內存分配的動作按照線程劃分在不同空間中進行,每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩衝(Thread Local Allocation Buffer , TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。
  • 內存分配完成後,虛擬機對分配到的內存空間都初始化為零值(不包括對象頭),保證對象的實例字段在Java代碼中可以不賦初始值就可以直接使用。
  • 虛擬機將對象的信息放入對象的對象頭中。
  • 執行構造函數

2.對象的內存佈局

Java內存分配與回收機制

對象的內存佈局總共分為三個部分:

  1. 對象頭中主要包括兩部分信息:
  • 一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
  • 另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。如果對象是Java數組,那在對象頭中還必須有一塊記錄數組長的數據。
  1. 實例數據部分是對象真正存儲的有效信息,也是程序代碼中定義的各種類型的字段內容。從父類繼承下來的,在子類中定義的都需要記錄下來。
  2. 對齊填充僅僅起到佔位符的作用。HotSpot VM的自動內存管理系統要求對象起始地址是8字節的整數倍,所以對象大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,需要通過對齊填充來補

三.內存的回收

1.對象存活判定

Java虛擬機通過可達性分析來判定對象是否存活。這個算法的基本思想是通過一系列稱為"GC Roots"的對象作為起始點,從這些節點向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到GC Roots沒有與任何引用鏈相連時,則該對象是不可用的。

如圖,object5,object6,object7雖然互有關聯,但是GC Roots是不可達的,所以它們被判定是可回收的對象。

另外值得一提的是引用計數算法,引用計數法是通過給對象一個引用計數器,每當有一個地方引用它時,計數器值就加一;引用失效時,計數器值就減一;任何時刻計數器為0的對象就是不可能再被使用的。引用計數器效率高、實現簡單。但是很難解決對象間相互循環引用的問題,主流Java虛擬機幾乎都不再使用引用計數法來管理內存

Java內存分配與回收機制

可達性分析示意圖

即使在可達性分析算法中不可達的對象,也不一定會立即被回收。一個對象被回收,至少要經歷兩次標記過程。

如果對象在進行可達性分析後沒有與GC Roots相連的引用鏈,那它將會被第一次標記並進行一次篩選。篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已被虛擬機調用過,虛擬機將這兩種情況視為"沒有必要執行"。

如果這個對象判定為有必要執行finalize()方法,那麼這個對象會放置在F-Queue隊列中,稍後由虛擬機自動建立、低優先級的Finalizer線程去執行finalize()方法。GC對F-Queue中的對象進行第二次小規模標記,如果對象重新與引用鏈上的任何一個對象建立關聯,那麼第二次標記時它將被移除"即將回收"的集合。否則對象就真的要被回收了。

Java內存分配與回收機制

Finalize方法

2.方法區回收判定

方法區的回收主要包括兩部分內容:廢棄常量無用的類

  • 廢棄常量的回收與回收Java堆中的對象類似。
  • 判斷無用的類的條件必須滿足三個條件:
  • 該類所有實例已經被回收。
  • 加載該類的ClassLoader已被回收。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,也無法通過反射訪問該類。

3.垃圾收集算法

  1. 標記-清除算法(Mark-Sweep)
  2. 算法分為"標記"和"清除"兩個階段:首先標記出需要回收的對象,在標記完成後統一回收被標記的對象。它主要不足有兩個:一是
    效率問題,標記和清除兩個過程效率都不高。二是空間問題,標記清除後會產生大量不連續內存碎片,碎片太多可能導致要分配較大對象時,無法找到足夠的內存空間不得不提前觸發一次垃圾收集動作。
Java內存分配與回收機制


  1. 標記-清除
  2. 複製算法
  3. 複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當一塊內存用完了,將存活的對象複製到另一塊上面,然後把已使用的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等情況,只要移動堆頂指針,按順序分配內存即可,實現簡單運行高效。只是這種算法將內存縮小為原來的一半,代價較高。
Java內存分配與回收機制


  1. 複製算法
  2. 標記-整理算法(Mark-Compact)
  3. 標記過程與"標記-清除"算法一樣,但後續不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
Java內存分配與回收機制


  1. 標記-整理算法

4.分代收集算法

商業虛擬機的垃圾收集都採用分代收集算法,根據對象存活週期將內存劃分為幾塊。Java堆分為新生代老年代,這樣可以根據年代特點採用適當的收集算法。新生代中每次垃圾收集都有大批對象死去,那就選用複製算法。老年代對象存活率高,沒有額外空間進行分配擔保,適合使用"標記-清理"或"標記-整理"算法來回收。

4.內存分配與回收策略

  1. 對象優先在Eden分區:
  2. 大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間分配時,虛擬機發起一次Minor GC。GC後對象嘗試放入Survivor空間,如果Survivor空間無法放入對象時,只能通過空間分配擔保機制提前轉移到老年代。
  3. 大對象直接進入老年代:
  4. 大對象指需要大量連續內存空間的Java對象。虛擬機提供-XX:PretenureSizeThreshold參數,如果大於這個設置值對象則直接分配在老年代。這樣可以避免新生代中的Eden區及兩個Survivor區發生大量內存複製。
  5. 長期存活的對象進入老年代:
  6. 虛擬機會給每個對象定義一個對象年齡計數器。如果對象在Eden出生並且經過一次Minor GC後任然存活,且能夠被Survivor容納,將被移動到Survivor空間中,並且對象年齡設為1.每次Minor GC後對象任然存活在Survivor區中,年齡就加一,當年齡到達-XX:MaxTenuringThreshold參數設定的值時,將會移動到老年代。
  7. 動態年齡判斷:
  8. 虛擬機不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold設定的值才會將對象移動到老年代去。如果Survivor中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。
  9. 空間分配擔保:
  10. 在Minor GC前,虛擬機會檢查老年代最大可用連續空間是否大於新生代所有對象總空間,如果條件成立,那麼Minor GC是成立的。如果不成立,虛擬機查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用連續空間是否大於歷次移動到老年代對象的平均大小,如果大於,將嘗試一次Minor GC。如果小於,或者HandlePromotionFailure設置值不允許冒險,那將進行一次Full GC。

新生代GC(Minor GC):發生在新生代的垃圾收集動作,因為Java對象大多朝生夕死,所以Minor GC非常頻繁,回收速度也較快。

老年代GC(Major GC/Full GC):發生在老年代的垃圾收集動作。出現Major GC,經常會伴隨至少一次Minor GC。Major GC的速度一般比Minor GC慢10倍以上。


分享到:


相關文章: