1. 程式人生 > >自動記憶體管理機制(4)- 記憶體分配和回收策略

自動記憶體管理機制(4)- 記憶體分配和回收策略

自動記憶體管理機制(4)- 記憶體分配和回收策略

Java所承諾的自動記憶體管理主要是針對物件記憶體的回收和物件記憶體的分配。

在Java虛擬機器的五塊記憶體空間中,程式計數器、Java虛擬機器棧、本地方法棧記憶體的分配和回收都具有確定性,一般在編譯階段就能確定需要分配的記憶體大小,並且由於都是執行緒私有,因此它們的記憶體空間都隨著執行緒的建立而建立,執行緒的結束而回收。也就是這三個區域的記憶體分配和回收都具有確定性,垃圾回收器不需要在這裡花費太大的精力。

而Java虛擬機器中的方法區因為是用來儲存類訊息、常量、靜態變數,這些資料的變動性較小,因此不是Java記憶體管理重點需要關注的區域。

對於堆,所有執行緒共享,所有的物件都需要在堆中建立和回收。雖然每個物件的大小在類載入的時候就能確定,但物件的數量只有在程式執行期間才能確定,因此堆中記憶體的分配具有較大的不確定性。此外,物件的生命週期長短不一,因此需要針對不同生命週期的物件採用不同的記憶體回收演算法,增加了記憶體回收的複雜性。

所以,Java自動記憶體管理最核心的功能是堆記憶體中物件的分配與回收。

1. 物件優先在Eden分配

目前主流的垃圾收集器都會採用分代回收演算法,因此需要將堆記憶體分為新生代和老年代。

在新生代中為了防止記憶體碎片問題,因此垃圾收集器一般都選用“複製”演算法。因此,堆記憶體的新生代被進一步分為:Eden區+Survior1區+Survior2區。

  1. 每次建立物件時,首先會在Eden區中分配。
  2. 若Eden區已滿,則在Survior1區中分配。
  3. 若Eden區+Survior1區剩餘記憶體太少,導致物件無法放入該區域時,就會啟用“分配擔保”,將當前Eden區+Survior1區中的物件轉移到老年代中,然後再將新物件存入Eden區。

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

所謂的大物件指的是需要大量連續記憶體空間的Java物件,比如陣列。

當發現一個大物件在Eden區+Survior1區中存不下的時候就需要分配擔保機制把當前Eden區+Survior1區的所有物件複製到老年代中去。

我們知道,一個大物件能夠存入Eden區+Survior1區的概率比較小,發生分配擔保的概率比較大,而分配擔保需要涉及到大量的複製,就會造成效率低下。 因此,對於大物件我們直接把他放到老年代中去,從而就能避免大量的複製操作。

3. 生命週期較長的物件

老年代用於儲存生命週期較長的物件,那麼我們如何判斷一個物件的年齡呢?

新生代中的每個物件都有一個年齡計數器,當新生代發生一次MinorGC後,存活下來的物件的年齡就加一,當年齡超過一定值時,就將超過該值的所有物件轉移到老年代中去。

使用-xxMaxTenuringThreshold設定新生代的最大年齡

設定該引數後,只要超過該引數的新生代物件都會被轉移到老年代中去。

3. 相同年齡的物件記憶體超過Survior記憶體一半的物件

如果當前新生代的Survior中,年齡相同的物件的記憶體空間總和超過了Survior記憶體空間的一半,那麼所有年齡相同的物件和超過該年齡的物件都被轉移到老年代中去。無需等到物件的年齡超過MaxTenuringThreshold才被轉移到老年代中去。


4. 空間分配擔保

當垃圾收集器準備要在新生代發起一次MinorGC時,首先會檢查“老年代中最大的連續空閒空間的區域的大小是否大於新生代中所有物件的大小?”,也就是老年代中目前是否能夠將新生代中所有物件全部裝下?

若老年代能夠裝下新生代中的所有物件,那麼此時進行MinorGC沒有任何風險,然後就進行MinorGC。

若老年代無法裝下新生代中所有的物件,那麼此時進行MinorGC是有風險的,垃圾收集器會進行一次預測:根據以往MinorGC過後存活物件的平均數來預測這次MinorGC後存活物件的平均數。

如果以往存活物件的平均數小於當前老年代最大的連續空閒空間,那麼就進行MinorGC,雖然此次MinorGC是有風險的。

如果以往存活物件的平均數大於當前老年代最大的連續空閒空間,那麼就對老年代進行一次Full GC,通過清除老年代中廢棄資料來擴大老年代空閒空間,以便給新生代作擔保。

注意:

  1. 分配擔保是老年代為新生代作擔保;
  2. 新生代中使用“複製”演算法實現垃圾回收,老年代中使用“標記-清除”或“標記-整理”演算法實現垃圾回收,只有使用“複製”演算法的區域才需要分配擔保,因此新生代需要分配擔保,而老年代不需要分配擔保。