1. 程式人生 > >JVM運行內存分配和回收

JVM運行內存分配和回收

成了 減少 管理機 標記 引用 動作 ase 方向 要求

本文來自網易雲社區

作者:呂宗勝

Java語言與C語言相比,最大的特點是編程人員無需過多的關心Java的內存分配和回收,因為所有這一切,Java的虛擬機都幫我們實現了。JVM的內存管理,大大降低了開發人員對內存管理的要求,也不容易出現C語言中的內存泄漏和溢出。但一旦應用內存發生問題,也會導致程序員難以定位。所以對於Java程序員來說認識和了解JVM的內存分配和回收對於代碼的編寫和應用的優化都有非常重要的意思。

1. JVM內存模型

Java的JVM的類型是非常多樣的,不同的JVM對於內存的分配和回收機制都不盡相同。我們這裏僅僅介紹的是最為流行的JVM,HotSpot VM,它是目前使用範圍最廣的Java虛擬機。但是JVM的更新速度也非常快,不同的版本之間也可能會存在一些區別,但總體來說,其構架還是相對穩定的。

說到內存管理,我們首先要了解的就是Java運行時的數據區域,包括線程私有數據和共享數據的分配等等方面。根據《Java虛擬機》中的描述,其運行時數據區域為:

技術分享圖片

從上圖可以看出,運行時的數據區域主要分成了5個部分:方法區、堆、虛擬機棧、本地方法棧和程序計數器。下面我們分別來介紹這5個部分。

1. 方法區

方法區中存儲的數據被各個線程所共享,用於存儲被虛擬機加載的類信息、常亮、靜態變量、編譯後的代碼等數據。

2. 堆

堆區域存儲的所有數據被各個線程共享,也是我們程序中最為關心的內存區域。該區域的目的就是存放對象實例,所以我們程序中的幾乎所有對象都是存儲在這塊區域的。同時,該區域也是JVM進行內存管理和回收的主要區域。

3. 虛擬機棧

虛擬機棧是除堆之外最重要的一塊內存區域,虛擬機棧中的數據是線程私有的。虛擬機棧是Java方法執行的內存模型,在每個方法執行時都會創建棧幀,用於存儲局部變量表,操作數棧等。

4. 本地方法棧

它與虛擬機棧的作用是十分相似的,而它們的區別是虛擬機棧是用於執行Java方法時的數據結構,而本地方法棧是Java使用的Native方法服務。

5. 程序計數器

程序計數器是非常小的一塊內存,每個線程都有一個獨立的程序計數器,通過程序計數器,我們可以知道當前線程的執行的字節碼序號。

上面簡單的了解了一下JVM運行時的數據區域和每個區域的基本功能,而在實際的使用過程中,我們最為關心的就是堆區域和虛擬機棧中的局部變量表。而對於線程私有的虛擬機棧而言,數據內存隨著線程的消亡而回收,而堆數據的回收則成為JVM內存管理和回收的重點。

2. 內存管理的分代機制

JVM中,目前使用的內配管理是分代方式,即把內存分成新生代、老生代和永久代。這裏我們講的分代管理機制是針對線程共享的內存區域,主要是堆,也包括方法區。

技術分享圖片

JAVA分代機制的好處是可以根據Java的實際對象創建和銷毀時機,在不同的生代中可以采用不同的垃圾回收策略,已提高垃圾回收的效率。在Java中,幾乎所有對象的實例都分配與新生代,而大部分對象的存活時間都不長,新生代中的對象回收會比較頻繁。而老年代中的存放是那些存活時間較長,或者對象過大導致無法在新生代中分配的對象。而永久代比較特殊,它一般是指內存區域中的方法區,HotSpot在實現方法區時作為永久代來處理,避免了額外來管理方法區。這塊區域的內存回收我們一般不做考慮,因為效果不會很明顯,而且回收的條件也非常苛刻。

3.垃圾清除算法

針對不同的分代以及其特性,不同分代使用的垃圾回收策略也是不一樣的。

3.1標記-清除算法

標記清除算法其實非常簡單,它是先標記那些已經死亡的對象,然後對這些死亡的對象進行清理。但是它的一個很大的不足在於直接清理會產生非常多得內存碎片,導致後續分配內存會因為碎片的問題而沒有連續的大空間滿足分配,從而觸發下一次的垃圾回收。可想而知,該垃圾清除策略效率和空間上都不會是最優的。

3.2 復制算法

復制算法,其實本質上跟標記-清除算法沒有區別,不過它解決了內存碎片化的問題,同時也解決了兩次掃描的問題。它的實現方式是在內存分配時先預留一部分內存,當內存需要回收的時候,它會進行掃描,把沒有過期的內存數據復制到預留的內存,而直接清理原先分配的內存,把原先分配的內存作為預留內存。這種方法的好處就是效率很高,缺點也非常明顯,那就是要浪費一部分內存作為預留內存,而如果為了保證數據100%的不丟失,原則上我們需要預留所有可分配內存的一半,造成內存的大面積浪費。

在新生代中,JVM采用了復制算法,因為新生代中的對象基本都是朝生夕死的,所以每次垃圾回收效果會比較明顯,我們也稱之為MinorGC。這裏新生代劃分成3塊區域,Eden區,From Survivor區和To Survivor區。兩塊Survivor互為備份,垃圾回收時,對象會集中復制到空閑的Survivor區中去。為了提高內存的利用率,這個Eden區會占用較大的比例,默認比例是8:1。這樣新生代只有10%的內存被浪費掉,但是畢竟很是有大量對象不能被回收而導致Survivor區空間不足的問題。這裏就涉及到分配擔保問題,當Survivor區不夠的時候,對象會直接進入老年代。

3.3 標記-整理算法

復制算法除了空間的浪費外,還有一個問題就是如果對象是長期存活的,將會導致內存回收的效率降低,因為復制的內存將會變大。所以復制算法比較適合那些對象存活期較短的內存區域回收。所以在復制和標記-清除算法的基礎上,提出了標記-整理算法。標記-整理算法也是先對對象進行標記,而後該算法將存活的對象往內存的一個方向移動,最終的內存將是占用的內存和空閑的內存有明顯的分界,它主要是解決了內存碎片化的問題。

與新生代的朝生夕死相比,老生代的對象存活時間會比較長,所以采用了標記-整理算法。如果發生了老生代的垃圾回收,我們稱之為FullGC。老生代的回收效率較低,會導致系統暫停較長的時間,所以我們要盡量減少FullGC的發生。

4. 分配回收策略

上面我們看到了JVM分代的垃圾回收算法,下面我們來看看JVM在內存分配和回收中的一些最常見的幾個點。

4.1 對象優化分配在Eden區

Java的對象優先分配在Eden區中,當Eden區中沒有足夠的內存分配時,JVM會進行一次MinorGC。所以JVM中MinorGC會是比較頻繁的垃圾回收動作,一般回收速度也比較快。對象分配在Eden區也不是絕對的,有一種例外是大對象會直接進入老年代。這裏的大對象是指需要連續內存空間的Java對象,比如說很長的字符串和數組等。大對象直接進入老年代非常不適合垃圾回收策略,特別是這些大對象也是那些朝生夕死的對象,這會造成比較頻繁的FullGC,導致系統性能降低。

4.2 長期存活的對象進行老年代

每一個對象都有一個對象年齡,對象在新生代中每經過一次垃圾回收,對象年齡增長1,當對象年齡超過某個閾值時,該對象會進入老年代。所以這裏就有一個問題,如果我們在非常頻繁的進行垃圾回收時,對象的對象年齡就會快速增長,一個對象會非常容易的進行老年代,造成FullGC的次數增長。

5. 對象死亡的判斷算法

上面我們介紹了垃圾回收,卻一直沒有介紹JVM中使用的判斷對象死亡的算法。最簡單的對象判斷的算法是采用計數法。當對象被引用時,計數加1,當一個對象的引用計數為0時,表示該對象已經死亡,可以進行回收。計數的方法雖然簡單,易實現,但是卻不能解決相互引用的問題,比如說對象A引用B,B也引用A,而A和B不再被其他對象引用,這種情況下,如果AB對象是可以被回收的,但是計數確不為0。

目前,通用的判斷對象死亡的方法是可達性分析算法。可達性分析是指從對象起點開始,如果該對象可以被引用到,則該對象是活著的,否則,該對象則死亡了。那麽該算法中最基本的對象起點是哪些呢?這些對象是指虛擬機棧中引用的對象、方法區中引用的對象和本地方法棧中引用的對象。

本文來自網易雲社區,經作者呂宗勝授權發布

相關文章:
【推薦】 OBS源碼編譯開發

JVM運行內存分配和回收