JVM執行記憶體分配和回收
本文來自 ofollow,noindex">網易雲社群
作者:呂宗勝
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原始碼編譯開發