本章要探究的問題 :
GC在回收記憶體時 :
- 怎麼判斷哪些記憶體需要回收 ?
- 什麼時候回收?
在幾個執行緒私有的執行時區域:
- 虛擬機器棧
- 程式計數器
- 本地方法棧
它們的記憶體分配和回收大多都具有確定性,隨著執行緒的建立而產生,隨著執行緒的停止而被回收。棧幀中的記憶體大小基本在類的結構確定下來時就已知。
而線上程共有的 Java堆(Heap)
和 方法區(Class(Method) Area)
這兩個區域則不同:
比如,一個介面有不同的實現類(類的資訊在方法區中),這幾個實現類的記憶體大小肯定不一,沒法在執行前就已知需要多大的記憶體,只有在執行期間才知道建立的物件的大小。
一,哪些記憶體需要回收?
在知道哪些記憶體需要回收之前,我們要知道怎麼判斷一個物件是否還存活,當它不再存活時,就回收它。
而 引用計數演算法
就是用來判斷物件是否存活的一個演算法。
1,引用計數演算法(Reference Counting)
演算法描述:給物件新增一個引用計數器,當有一個地方引用了它,計數器+1,當引用失效,計數器-1,在任何時刻,計數器為0時此物件將不能再被使用。
引用計數法在大多數情況下表現都不錯,也有被很多公司採用的應用案例。但是在JVM中並沒有採用這種演算法,原因是:無法解決物件之間存在相互引用的問題。
public class Person {
Object instance = null;
public static void main(String[] args) {
Person a = new Person();
Person b = new Person();
a.instance = b;
b.instance = a;
a = null;
b = null;// 正常情況下在這裡GC就會把a,b回收掉
}
}
複製程式碼
正常情況下在執行11-12行程式碼時,JVM的GC會把a,b兩個物件回收,但是在引用計數演算法的情況下:
- 執行
a=null
時,a的引用計數器值為1,因為b物件在引用它。 - 執行
b=null
時,b的引用計數器值為1,因為a物件在引用它。
2,可達性分析(Reachability Analysis)演算法
在Java語言中是通過可達性分析來判斷物件是否存活。
演算法描述 : 通過一系列的 GC Roots
作為起始點,從這些起始點開始向下搜尋,能搜尋的到的物件說明其可用,不會被GC回收掉,搜尋所走過的路徑稱為 引用鏈(Reference Chain)
。相反,如果一個物件沒有到達GC Roots的路徑,則說明它不可用,被判定為可被GC回收的物件。
如圖 : 1區域的物件雖然互相關聯,但是它們不可到達GC Roots,所以他們會被回收掉,而2區域的物件與GC Roots之間是有可到達路徑的,所以它們不會被回收。
什麼是GC Roots ?
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件
- 方法區中類的靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法棧中JNI(Native方法)引用的物件
這些都可作為GC Roots.
3,什麼是引用(Reference)
我們在上面的 引用計數演算法
和 可達性分析
中,都提到了 物件之間的引用
關係。
在Java1.2之前,關於 引用
的定義 :
如果
reference
型別的資料儲存的數值代表的是另一塊記憶體的起始地址,就說這塊記憶體代表一個引用。
JDK1,2之後,又引入了 強引用
, 軟引用
, 弱引用
, 虛引用
,這四個概念,並且這四種表現的引用關係越來越弱。
- 強引用(Strong Reference) :
例:
Object o = new Object();
複製程式碼
只要強引用還在,GC永遠不會回收掉被引用的物件。
- 軟引用(Soft Reference) :
有用,但非必須,在將要發生記憶體溢位時,會把 軟引用
的物件回收掉,如果記憶體依然不夠用,則丟擲OOM異常。
- 弱引用(Weak Reference):
非必需物件,只要GC發生了垃圾回收,不管此時記憶體是否充足, 弱引用
的物件都會被回收掉。
- 虛引用(Phantom Reference):
- 最弱的引用關係
- 無法通過虛引用構造市例項。
- 唯一的作用就是在虛引用關聯的物件被GC回收掉時,可以接受到一個訊號。
4,如何判斷一個物件可回收(已死)?
一個物件僅僅通過上面說的可達性分析看它沒有與GC ROOTS關聯來判定這個物件是否可被回收是不夠的。
一個物件要經過下面一段判斷過程來判斷它是否要被回收(建議收藏(^__^) 嘻嘻……):
5,方法區的回收
上面我們說的是存在於Java堆中的物件的回收,但其實在方法區還要回收以下東西:
① 回收廢棄常量
假如常量池中有一個字串 "abc" ,但是系統中沒有一個String 物件指向它,也就是這個常量沒有被引用,
當GC在回收時會回收此字面量。
② 回收廢棄的類(無用的類)
- 該類的例項都已被回收,Java堆中不存在任何該類的例項。
- 載入該類的ClassLoader已經被回收。
- 該類的Java.lang.class物件沒有被引用(在反射中會被用到這點)。
③ 方法區的回收策略:
GC在回收方法區時會採用一下2種方式:
- 標記-整理
- 標記-清除
二,如何回收?
GC在回收記憶體時會採用多種垃圾收集演算法,這些演算法各有優劣。
1.標記-清除(Mark-Sweep)演算法
此演算法是最基礎也是最古老的垃圾回收演算法,該演算法主要經過2個過程
① 演算法描述
- 標記階段:經過如何判斷一個物件可被回收所述,對可被回收的物件進行標記。
- 清除階段:將被標記的物件統一回收。
② 演算法缺陷
- 效率問題:此種演算法標記和清除的效率都不高。
- 標記清除後產生大量不連續的記憶體空間碎片。
2.複製(Copying)演算法
複製演算法針對效率問題進行了優化,它將記憶體區域劃分為2塊,每次只使用其中一塊。
- 活動區域
- 空閒區域
① 演算法描述
如圖:
- 回收前 : 記憶體被劃分為左右兩側區域,右側為空閒區域,暫時不使用它
- 回收時 : 將左側要被回收的部分(黑色) 回收掉,然後將4個存活物件(淡灰色)移動到右側的空閒區域,並且做了2件事
- 將移動到空閒區域的存活物件按記憶體地址進行排列。
- 將存活物件指向的舊地址指向新記憶體地址。
- 回收後 : 原先的右側空閒區域變為活動區域,左側的活動區域變為空閒區域。
左右兩側的區域狀態在每一次回收後都來回轉換...
② 演算法缺陷
- 很顯然,這種演算法浪費了一般記憶體。
- 當活動區域的100%的物件都還在活躍,那麼在回收時需要將全部的物件複製到右側的空閒區域,此時的效率就很低。
③ 演算法應用
IBM公司經研究表明,Java堆新生代種的物件98%是 '朝生夕死' 的物件,比如臨時變數等作用域很少的物件。
所以現在的虛擬機器並不會按照 1:1的比例劃分兩個區域。
現在的JVM虛擬機器中,將新生代劃分為一塊 Eden
區域,和2塊較小的 Survivor
區域(from ,to區)。
每次使用Eden區和1塊Survivor區(from區)最為活動區域,當發生記憶體回收時,將這2塊記憶體中的存活物件複製到另一塊Survivor區(to區)。
在 HotSpot
虛擬機器中,Eden區和Survivor的劃分是: 8: 1,這樣,活動區域佔新生代的 (8+1)/10 *100% = 90%,只有10%的記憶體浪費。
老年代 : 當將存活物件從活動區域(Eden,from) 複製到 to區時,如果to區不夠用,則將剩下的存活物件放到老年代。
3.標記-整理演算法
標記-整理主要運用於老年代中。
① 演算法描述
此演算法與標記-清除演算法類似,也是經歷2個階段:
- 標記階段:此階段於標記-清除中的標記階段相同,都是標記出要回收的物件。
- 整理階段:把所有存活的對像,按記憶體地址排列移動到記憶體區域的一端,將端邊界以外的區域進行回收。
② 演算法應用
由於老年代的特點,物件的存活率較高,沒有額外的空閒區域,所以 老年代適用 標記-清除和標記-整理演算法。
(完~)
Reference:
深入理解Java虛擬機器