1. 程式人生 > >Java虛擬機器之自動記憶體管理機制

Java虛擬機器之自動記憶體管理機制

Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍城的高牆,牆外面的人想進去,牆裡面的人卻想出來。

Java憑藉虛擬機器自動記憶體管理機制,不需要為每一個new操作去配對free的操作,不容易出現記憶體洩露和記憶體溢位問題。但是我們還是很有必要了解虛擬機器是怎麼使用記憶體的。本文樓主將著重介紹虛擬機器中記憶體是如何劃分以及垃圾收集的演算法。

1.Java記憶體區域

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體區域劃分為若干個不同的資料區域,下圖為Java虛擬機器執行時的資料區。


經常能夠聽到Java記憶體區分為對記憶體與棧記憶體,這種分法比較粗糙

1.1 Java堆

是Java虛擬機器所管理的記憶體中最大的一塊,是被所有執行緒共享的一塊記憶體區域。所有的物件例項及陣列都要在堆上分配。

Java堆是垃圾收集器管理的主要區域,也叫GC堆,現在收集器基本都採用分代收集演算法,所以Java堆中還可以細分為新生代、老年代;再細緻一點有Eden空間、From Survivor空間、To Survivor空間。

如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件,將會丟擲OutOfMemoryError異常

1.2 Java棧(虛擬機器棧)

執行緒私有,生命週期與執行緒相同。

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀Stack Frame用於儲存區域性變量表、運算元棧等啥的。每個方法從呼叫到執行完成對應一個棧幀在虛擬機器中入棧到出棧的過程。

1.3 程式計數器

Program Counter Register一塊較小小記憶體空間,執行緒私有,可以看作是當前執行緒所執行的位元組碼的行號指示器。

如果執行緒執行的是一個Native方法,這個計數器值則為空,此記憶體區域時唯一一個在JVM中沒有規定任何記憶體溢位的區域。

1.4 本地方法棧

與虛擬機器棧發揮的作用類似

1.5 方法區

各個執行緒共享區域,儲存已被虛擬機器載入的類資訊、常量、靜態變數等,別名非堆、永久代。

2.垃圾收集器與記憶體分配策略

Java虛擬機器執行時程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出有條不紊地執行著出棧和入棧操作;因此這幾個區域的記憶體分配和回收都具備確定性,就不需要過多考慮回收問題,因為方法結束或者執行緒結束時,記憶體自然就跟著回收了。而Java堆和方法區則不一樣,我們只有在程式執行期間才能知道會建立那些物件,這部分的記憶體分配和回收都是動態的,垃圾收集器關注的就是這部分記憶體。

2.1 物件存活判斷方法介紹

2.1.1 引用計數演算法

給物件新增一個引用計數器,每當有一個地方引用計數器值加1;引用失效計數器值減1;任何時刻計數器為0的物件就是不可能再被使用的。

JVM沒有選用引用計數法管理記憶體,最主要原因是它很難解決物件之間相互迴圈引用的問題。

2.1.2 可達性分析演算法

在主流的商用程式語言Java,C#等都是通過Reachability Analysis來判定物件是否存活

演算法基本思想:通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件不可用。

在Java中可作為GC Roots的物件包括:

1)虛擬機器棧中引用的物件

2)方法區中類靜態屬性引用的物件

3)方法區中常量引用的物件

4)本地方法棧中JNI(Native方法)引用的物件

2.2 垃圾收集演算法

2.2.1 標記-清除演算法

思想:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

不足:1)效率問題,標記和清除兩個過程效率都不高;2)空間問題,標記清除之後會產生大量不連續的記憶體碎片。

2.2.2 複製演算法

思想:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。

不足:將記憶體縮小為了原來的一般,代價太高了一點

應用:現在商業JVM都是採用這種收集演算法來回收新生代。IBM統計新生代中的物件98%是朝生夕死,所以並不需要按照1:1劃分記憶體空間。Eden:Survivor=8:1,大部分物件在Eden區中生成。當Eden區滿時,還存活的物件將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活物件將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的物件,將被複制“年老區(Tenured)”。

2.2.3 標記-整理演算法

老年代採用這種垃圾回收演算法

思想:標記過程與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

2.2.4 分代收集演算法

當前商業JVM都採用分代收集。

思想:並沒有什麼新的思想,只是根據物件存活週期不同將記憶體劃分為幾塊——老年代、新生代。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,選用複製演算法;而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-整理演算法。

3.垃圾收集器

Java堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法,年老代主要使用標記-整理垃圾回收演算法。


4.記憶體分配與回收策略

1)物件優先在Eden分配

2)大物件直接進入老年代

3)長期存活的物件將進入老年代

解釋:如果物件在Eden出生經過第一次Minor GC(新生代GC,指發生在新生代的垃圾收集動作,Minor GC非常頻繁,一般回收速度也比較快)後仍然存活,並且能夠被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中每“熬過”一次Minor GC,年齡加1,加到一定程度(預設15)就會晉升到老年代。

4)動態物件年齡判定

解釋:如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡所有物件將直接進入老年代。

5)空間分配擔保

解釋:在發生Minor GC之前,JVM會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,這個條件成立,那麼Minor GC可用確保安全。否則,會去查每次晉升到老年代物件容量的平均大小作為經驗值,大於進行Minor GC,這是有風險的。否則或不允許冒險,就要改為進行一次Full GC(Major GC,老年代GC,速度比Minor GC慢10倍以上)