JVM記憶體管理和垃圾回收機制介紹

weixin_34321977發表於2016-08-10

 http://backend.blog.163.com/blog/static/20229412620128233285220/

 

 
 

    記憶體管理和垃圾回收機制是JVM最核心的兩個組成部分,對其內部實現的掌握是Java開發人員開發出高質量的Java系統的必備條件。最近整理了一些關於JVM記憶體管理和垃圾回收方面的知識,這裡梳理一下,分享給大家,希望能夠對Java虛擬機器有更深入的瞭解。

1. JVM記憶體管理

       首先,JVM將記憶體組織為主記憶體和工作記憶體兩個部分。主記憶體中主要包括本地方法區和堆。每個執行緒都有一個工作記憶體,工作記憶體中主要包括兩個部分,一個是屬於該執行緒的棧和對主存部分變數拷貝的暫存器。

 

圖1 JVM記憶體模型

    每一個JVM例項都有一個方法區和堆,被該例項內的所有執行緒共享,而每一個新的執行緒啟動時,JVM都會為該執行緒分配一個僅屬於該執行緒的工作記憶體。Java中主要有3種型別的變數,JVM對記憶體的組織管理對映到變數的儲存上,可以按照如下理解

1)  靜態變數,被同一個類中的多個物件共享,儲存在方法區中,被所有執行緒共享;

 

2)  例項變數,宣告在類中,儲存在堆上;

3)  區域性變數,宣告在方法內,儲存在棧上;

 

 

每個執行緒只能操作自己的工作記憶體,執行緒不能直接操作主存,除非變數用volatile關鍵字修飾,工作記憶體之間不能直接訪問,必須通過主記憶體。

 

 

 

2. 垃圾回收機制

 

 

Java的垃圾回收是由JVM來完全負責的,對使用者來說基本上是透明的。之所以說基本,是因為:

1) 使用者可以使用Finalize函式在垃圾物件回收之前,釋放由本地方法申請的記憶體空間或者關閉檔案等操作。

2) 使用者可以使用System.gc()建議JVM進行垃圾回收,但需要注意的是,這僅僅是建議,至於什麼時候回收,會不會回收都是未知的,都是由JVM來完全負責的。

JVM以獨立的,低優先順序的執行緒來對堆空間進行檢測。整個垃圾回收機制主要涉及兩個根本問題:其一是要確定到底什麼是垃圾。其二要是釋放垃圾物件佔用的記憶體空間。

首先我們先來看第一問題,Java中沒有被其他物件引用的物件即為垃圾物件,Java中有四種型別的物件引用:StrongReference, SoftReference, WeakReference, PhantomReference.

1) StrongReference:即Java普通的物件引用。

2) SoftReference:如果一個物件被SoftReference引用,又沒有其他的StrongReference指向它的話,該物件可以在任何時候被垃圾回收,但這個時候一般發生在其他執行緒因為記憶體空間不足被掛起,發生OutofMemory錯誤之前。SoftReference一般用於Cache內物件的引用。

3) WeakReference:如果一個物件被WeakReference引用,又沒有其他的StrongReference和SoftReference引用指向他的話,該物件會被立即回收。相對於SoftReferen ce而言,WeakReference是一種急切的垃圾回收。

4)PhantomReference:如果一個物件被PhantomReference引用,而沒有其他的StrongReference, SoftReference和WeakReference引用指向它的話,該物件可以在任何時候被垃圾回收。PhantomReference引用一般用於檢測物件進入ReferenceQueue,用於沒有實現Finalize方法的物件在垃圾回收前釋放由本地方法申請的記憶體空間和關閉檔案的操作。

在瞭解了到底什麼是垃圾之後,我想介紹一下JVM的垃圾回收演算法。目前,JVM主流的垃圾回收演算法是一種叫做分代垃圾回收的演算法。我覺得采用這種演算法一個立足點在於:JAVA中對大多數物件都存活期較短,能長時間存在的物件佔少數。正是基於這樣一個事實,我覺得才有必要對JAVA的主存空間按照物件的存活期進行劃分。

分代垃圾回收演算法主要包括以下三個回收演算法:複製收集,Mark-Sweep,Mark-Compacting。

1) 複製收集:將一個區域中可達的物件複製到另外一個空閒的區域中,將原區域中所有內容擦除。複製收集的優點在於其效益較高,缺點在於需要一塊額外的記憶體空間。所以適用於小型的垃圾回收。JVM中的MajorGC普遍採用該種演算法。

2) Mark-Sweep:叫做標記清除收集,首先對區域中所有引用進行掃描,將不可達的物件標記出來,然後將這些標記的物件擦除。Mark-Sweep的優點在於不需要移動元素,就可以實現垃圾回收,同時也不需要額外的空閒記憶體區域。缺點是會產生大量的記憶體碎片。同時Mark-Sweep的記憶體分配採用的是一種Freelist的方式,就是將所有的空間區域做成一個List,每次分配空間就是從FreeList中去找一個滿足條件的區域分配。這種分配方式效率低,而且複雜。

       3) Mark-Compacting叫做標記壓縮收集,首先對整個空間進行掃描,標記出可達物件和不可達物件,然後將不可達的物件擦除,將可達的物件移動到一個連續的空間中。這種演算法的優點在於不會產生記憶體碎片。它和複製收集的記憶體分配都採用的是一種叫做空閒指標的方式,就是將指標指向最後一個物件的空間,每次直接從後面的空間進行分配。這種記憶體分配方法效率高。Mark-Compacting的缺點是需要移動元素。同時,除了標記不可達物件,還要標記可達的物件。效率較低。Mark-Sweep和Mark-Compacting主要用於舊生代記憶體的回收。

Java的主存區空間主要劃分為3個部分:新生代(Young),舊生代(Tenured)和Perm區。新生代又劃分為3個部分:Eden和兩個Survivor區。所有用New建立的物件都是放在Eden區的,Survivor區主要是用於複製收集的目標空間。所以兩個Survivor始終至少有一個是空閒的。

 

圖2 JVM代劃分示意圖

 

 

 

新生代的垃圾回收演算法採用的是複製收集,在我看來原因就是複製收集效率高。但是需要額外的記憶體空間,這個記憶體空間就是Survivor。新生代的垃圾回收叫做ManorGC,JVM提供了三種新生代的垃圾回收器:

1)序列GC:用一個執行緒完成整個複製收集過程,需要應用程式暫停執行。在單CPU的情況下,效率高於其他的兩種。但是會造成應用程式出現長時間的暫停。

2) 並行GC:用多執行緒來完成整個複製收集過程,會降低應用程式暫停的時間。

3) ParNew:與並行GC基本相同,唯一的不同在於設計ParNew主要是用來配合舊生代CMS併發收集使用的。在舊生代的CMS過程中,有可能新生代出發ManorGC,改變物件的引用關係,從而造成舊生代的錯誤,ParNew就是專門處理該情況的。加了特殊的處理,保證了物件關係的正確性。所以舊生代的CMS必須配合新生代的ParNew一起。

舊生代的垃圾回收演算法主要採用的是Mark-Sweep和Mark-Compacting,JDK提供了三種舊生代的垃圾回收器:

1) 序列GC:該回收器採用的是Mark-Compacting演算法,用一個單獨的執行緒完成標記和壓縮過程,需要長時間暫停應用程式的執行,在單CPU的情況下,效率要高於其他兩種。

2)並行GC:該回收器採用的並行的Mark-compacting演算法,首先,用多執行緒去標記可達物件和不可達物件,用一個執行緒去確定移動的目標區域和源區域,在用多執行緒併發移動壓縮。優點是併發執行,能夠減少應用程式的暫停時間,缺點是移動元素,效率不高。

3)CMS GC:併發GC,採用的是Mark-Sweep演算法,首先用一個執行緒去標記直接可達的物件。然後使用多執行緒去標記間接可達的物件。由於第二階段是併發標記,所以有可能在標記過程中,應用修改了物件的引用關係,所以第三步是用單執行緒來對修改的引用關係進行標記。最後用多執行緒進行清除。該種演算法的優點由於絕大多數都是併發過程,所以能很大程度上降低應用的暫停時間,但是缺點是3次標記,總的時間要長於並行GC。

相關文章