- 這篇文章主要講垃圾收集器,下一篇文章再講內(nèi)存分配策略。
1. JVM運(yùn)行時(shí)各個(gè)數(shù)據(jù)區(qū)域的內(nèi)存分配和回收概況
1.1程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)
- 這三個(gè)區(qū)域隨線程而生、隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊的進(jìn)行著出棧、入棧操作。每一棧幀中分配的內(nèi)存基本上在類結(jié)構(gòu)確定下來(lái)是就已知的(盡管在運(yùn)行期會(huì)由JIT編譯器進(jìn)行一些優(yōu)化,但大體上是編譯期可知)。因此這三個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)域就不過(guò)多考慮回收問(wèn)題,因?yàn)榉椒ńY(jié)束或線程結(jié)束時(shí),內(nèi)存自然就跟著回收了。本文重點(diǎn)討論堆和方法 區(qū)。
1.2堆和方法區(qū)
- 這部分內(nèi)存的分配和回收是動(dòng)態(tài)的,因?yàn)橐粋€(gè)接口的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,只有在程序運(yùn)行期間才能知道會(huì)創(chuàng)建哪些對(duì)象。這部分的內(nèi)存分配和回收是本文要討論的。
2. 垃圾收集器
- java中垃圾收集器完成的事
- 哪些內(nèi)存需要回收?
- 什么時(shí)候回收?
- 如何回收?
- 第2節(jié)內(nèi)容將圍繞這3個(gè)問(wèn)題進(jìn)行敘述。注:這一節(jié)所講內(nèi)容是針對(duì)JVM中的堆內(nèi)存,第3節(jié)講方法區(qū)內(nèi)存回收
2.1 對(duì)象存活判定算法(哪些內(nèi)存需要回收?)
- 在堆中存放著Java中幾乎所有對(duì)象實(shí)例(有的存放在方法區(qū)中),垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事就是確定這些對(duì)象之中哪些還“存活”著,哪些“死了”(即不可能在被任何途徑使用的對(duì)象)。第二件事才是進(jìn)行內(nèi)存回收。判斷對(duì)象存活的算法有如下幾種,其中JVM使用的是可達(dá)性分析算法。
2.1.1 引用計(jì)數(shù)算法(Reference Counting)
- 描述:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值加1,引用失效時(shí)減1,當(dāng)計(jì)數(shù)器值為0時(shí)對(duì)象就不可能在被使用了。
- ** 優(yōu)點(diǎn)**:實(shí)現(xiàn)簡(jiǎn)單,判定效率高。
- 使用情況:微軟的COM(Component Object Model)技術(shù)、使用ActionScript 3的FlashPlayer、Python語(yǔ)言使用該算法判定對(duì)象存活的。
-
缺點(diǎn):很難解決對(duì)象之間相互循環(huán)引用問(wèn)題。
如下案例:
/**
* testGC()方法執(zhí)行后,objA和objB會(huì)不會(huì)被GC呢?
* @author zzm
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個(gè)成員屬性的唯一意義就是占點(diǎn)內(nèi)存,以便在能在GC日志中看清楚是否有回收過(guò)
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設(shè)在這行發(fā)生GC,objA和objB是否能被回收?
System.gc();
}
}
- 在上面這種情況下,實(shí)際上兩個(gè)對(duì)象已經(jīng)不可能在被訪問(wèn)了,但是因?yàn)榛ハ嘁谜邔?duì)象,導(dǎo)致計(jì)數(shù)器都不為0,如果采用引用計(jì)數(shù)器,objA和objB不會(huì)被GC收集器回收。
- 但是在Java中這兩個(gè)對(duì)象會(huì)被回收,因?yàn)镴VM不是采用引用計(jì)數(shù)算法。所以很好的解決了循環(huán)引用問(wèn)題。
2.1.2 可達(dá)性分析算法(Reachability Analysis)
-
描述:通過(guò)一系列稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相相連時(shí)(即從GC Roots到這個(gè)對(duì)象不可達(dá)),則證明此對(duì)象是不可用的。
可達(dá)性分析算法判斷對(duì)象是否可收回
上圖中object5、object6、object7雖然想好有關(guān)聯(lián),但是到GC Roots是不可達(dá)的,所以會(huì)被判定為是可回收的對(duì)象。 - Java語(yǔ)言中可作為GC Roots的對(duì)象包括下面幾種
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
- 方法區(qū)類靜態(tài)屬性引用的對(duì)象(static).
- 本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象。
- 方法區(qū)中常量引用對(duì)象(final)。
- 使用情況:主流程序語(yǔ)言(java、C#)都使用該算法來(lái)判定對(duì)象是否存活的。
2.2 對(duì)象什么時(shí)候回收(什么時(shí)候回收?)
- 經(jīng)過(guò)可達(dá)性分析算法確定了一個(gè)對(duì)象是一個(gè)可回收對(duì)象后,那么是不是就需要立即回收該對(duì)象呢?在對(duì)象要被回收前是否可以做一些事情呢?(比如復(fù)活、釋放資源),這些就和java中的引用、和對(duì)象的finalize()方法有關(guān)了。
2.2.1 引用
- 無(wú)論通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量、還是通過(guò)可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判定對(duì)象存活都與“引用”有關(guān)。jdk1.2之前,java中的對(duì)象只有被引用和沒(méi)有被引用兩種狀態(tài),這樣的話GC收集器對(duì)該對(duì)象只有回收和不回收兩種情況,但是對(duì)于一些“食之無(wú)味、棄之可惜”的對(duì)象,我們希望:當(dāng)內(nèi)存空間不足時(shí),保留在內(nèi)存中,如果內(nèi)存進(jìn)行垃圾收集后還非常緊張就拋棄這些對(duì)象。很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。
- jdk1.2之后,java對(duì)引用概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phanton Reference)4種,4種強(qiáng)度依次減弱。
- 強(qiáng)引用(Strong Reference):類似“Objcet obj = new Object()"這類的引用,只要引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收被引用的對(duì)象。
- 軟引用(Soft Reference):描述一些有用但非必要的對(duì)象,對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)發(fā)生內(nèi)存溢出之前,將會(huì)把這些對(duì)象列入回收范圍之中進(jìn)行第二次回收,為什么是第二次?因?yàn)榈谝淮位厥諘r(shí)發(fā)現(xiàn)該對(duì)象是軟引用就不將其列入回收范圍。第二次回收后還沒(méi)有足夠內(nèi)存,才拋出異常。在被回收之前可以通過(guò)軟引用獲得對(duì)象。java提供SoftReference類來(lái)實(shí)現(xiàn)軟引用。
- 弱引用(Weak Reference):描述一些有用但非必要的對(duì)象,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器工作時(shí),無(wú)論內(nèi)存是否足夠,都會(huì)被回收。WeakReference類來(lái)實(shí)現(xiàn),在被回收之前可以通過(guò)弱引用獲得對(duì)象。。
-
虛引用(Phanton Reference):最弱,一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用取得一個(gè)對(duì)象實(shí)例。唯一目的是當(dāng)這個(gè)對(duì)象被收集時(shí)能夠收到一個(gè)系統(tǒng)通知。
Java中引用的詳解
JAVA四種引用方式
2.2.2生存還是死亡
- 即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非”非死不可“,這時(shí)候它們暫時(shí)處于緩刑階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程。
- 第一次標(biāo)記并進(jìn)行一次篩選,篩選條件是:對(duì)象是否有必要執(zhí)行finalize()方法(Object的protected方法)。如果沒(méi)必要執(zhí)行,則會(huì)被回收。如果有必要?jiǎng)t執(zhí)行,則該對(duì)象會(huì)被放到一個(gè)F-Queue隊(duì)列中,JVM會(huì)創(chuàng)建一個(gè)低優(yōu)先級(jí)的Finalizer線程去執(zhí)行隊(duì)列中對(duì)象的finalize方法。對(duì)象在finalize方法中可以拯救自己,比如將this賦值給某個(gè)變量。finalize最主要目的是用來(lái)釋放資源,畢竟finalize只會(huì)被調(diào)用一次。
有必要執(zhí)行finalize的條件是1.該對(duì)象的finalize方法被覆蓋。2.該對(duì)象的finalize方法之前沒(méi)有被調(diào)用過(guò)。 - 第二次標(biāo)記: 在稍后GC將對(duì)F-Queue列中對(duì)象進(jìn)行第二次標(biāo)記,如果這時(shí)對(duì)象沒(méi)有拯救自己則就會(huì)被回收,否則會(huì)被移除”即將回收“集合(即拯救了自己)

2.3垃圾收集算法(如何回收?)
- 經(jīng)過(guò)上面的可達(dá)性分析算法確定一個(gè)對(duì)象可以回收,以及通過(guò)引用類型或者finalize()方法最終確定一個(gè)對(duì)象的回收時(shí)機(jī)后,下面要做的事情就是對(duì)對(duì)象進(jìn)行回收釋放內(nèi)存的工作了。JVM中如何進(jìn)行內(nèi)存回收呢?每個(gè)回收算法有什么優(yōu)缺點(diǎn)呢?
2.3.1標(biāo)記-清除算法
分兩個(gè)階段"標(biāo)記“和”清除“:先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。(具體標(biāo)記過(guò)程看上面)
不足和產(chǎn)生產(chǎn)生的問(wèn)題
效率不高。
-
空間問(wèn)題:標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)內(nèi)存碎片。碎片太多可能導(dǎo)致以后在程序中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
標(biāo)記-清除算法示意圖
2.3.2復(fù)制算法
- 總策略:可用內(nèi)存分成兩個(gè)相等的塊。
- 思想:將可用內(nèi)存按容量劃分為大小相等的兩塊。每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
- 優(yōu)點(diǎn)
- 對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)指針按順序分配內(nèi)存即可。
- 缺點(diǎn)
-
代價(jià)高,將內(nèi)存縮小為原來(lái)一半了。
復(fù)制算法示意圖
2.3.3改進(jìn)的復(fù)制算法
補(bǔ)充:JVM的堆中分為新生代和老年代,不同代中存放的對(duì)象生存時(shí)間不一樣,生存時(shí)間不一樣,那么對(duì)不同代的內(nèi)存區(qū)域采用的回收算法就應(yīng)該充分考慮到它們的特點(diǎn)。
總策略: 1塊Eden空間 + 2塊Survior空間 + 分配擔(dān)保。
新生代中的對(duì)象98%都是朝生夕死,所以并不需要按照1:1的比例來(lái)劃分內(nèi)存空間,所以把內(nèi)存分為1塊較大的Eden空間和2塊較小的Survivor空間。
回收過(guò)程
每次使用Eden和其中一塊Survivor,當(dāng)回收時(shí),將Eden和Survivor中還活著的對(duì)象一次性地復(fù)制到另外一塊Survior空間上,最后清理掉Eden和剛才使用過(guò)的Survivor空間,即2個(gè)Survior輪流空著。注:2個(gè)Survivor就可以確保每次回收前至少有一個(gè)是空的,用來(lái)接收沒(méi)被回收掉的。
Eden和Survior的分配比例
HotSpot虛擬機(jī)默認(rèn)Eden和Survivor大大小比例是8:1,也就是每次新生代中可用內(nèi)存為整個(gè)新生代容量的90%(80%+10%),只有10%被浪費(fèi)。分配擔(dān)保
基于上面的Eden和Survior的分配比例,當(dāng)回收后有大于10%的對(duì)象存活的話,那么Survivor空間會(huì)不夠用,這時(shí)就需要使用其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保(Handle Promotion):即把新生代收集下來(lái)的存活對(duì)象通過(guò)分配擔(dān)保機(jī)制復(fù)制到老年代中。然后在清理帶Enden和剛才使用過(guò)的一塊Survivor空間。(那么如果老年代的空間也不夠存放呢?下面會(huì)講。)使用現(xiàn)狀:現(xiàn)在商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代.(那老年代用什么算法呢)
為什么適合用在新生代中?
新生代中對(duì)象產(chǎn)生的多、存活率低,所以復(fù)制操作就很少,回收就快,沒(méi)有碎片問(wèn)題,分配內(nèi)存時(shí)也很快。
2.3.4 標(biāo)記-整理算法
- 算法思想
與標(biāo)記-清除類似,只是標(biāo)記完了,不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理帶哦端邊界以外的內(nèi)存。

- 老年代適合采用”標(biāo)記-整理“
老年代中對(duì)象存活時(shí)間長(zhǎng),使用復(fù)制算法的話需要進(jìn)行太多復(fù)制操作,效率變低,還有就是如果不想浪費(fèi)50%空間,就需要額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%活著的極端情況。所以標(biāo)記整理比較適合。
2.3.5分代收集算法(*)
- 當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用"分代收集“(Generational Collection)算法。
- 算法思路
- 根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般把Java堆中分為新生代和老年代,這樣可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/li>
- 新生代中每次垃圾收集都有大批對(duì)象死去,少量存活,所以選復(fù)制算法。
- 老年代中對(duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用”標(biāo)記-清理“或”標(biāo)記-整理“算法進(jìn)行回收。
3.方法區(qū)內(nèi)存回收
3.1概述
- 上面講的內(nèi)容都是針對(duì)堆中的內(nèi)存回收,那么方法區(qū)呢?堆中的內(nèi)存在JVM中被分為”新生代“、“老年代”。而方法區(qū)的內(nèi)存被稱為“永久代”。Java虛擬機(jī)規(guī)范中確實(shí)說(shuō)過(guò)可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾收集,而且在方法區(qū)進(jìn)行垃圾收集的“性價(jià)比”一般比較低,即回收一次只有很少的內(nèi)存被釋放掉。
- 永久代中垃圾收集的內(nèi)容:廢棄常量、無(wú)用類。
3.2常量回收
3.2.1方法區(qū)中常量類型
- 先來(lái)說(shuō)說(shuō)方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號(hào)引用。字面量比較接近Java語(yǔ)言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號(hào)引用則屬于編譯原理方面的概念,包括下面三類常量:
1、類和接口的全限定名
2、字段的名稱和描述符
3、方法的名稱和描述符
3.2.2回收過(guò)程
- 回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常類似。以常量池中字面量的回收為例,假如一個(gè)字符串“abc”已經(jīng)進(jìn)入了常量池中,但是當(dāng)前系統(tǒng)沒(méi)有任何一個(gè)String對(duì)象是叫做“abc”的,換句話說(shuō)是沒(méi)有任何String對(duì)象引用常量池中的“abc”常量,也沒(méi)有其他地方引用了這個(gè)字面量,如果在這時(shí)候發(fā)生內(nèi)存回收,而且必要的話,這個(gè)“abc”常量就會(huì)被系統(tǒng)“請(qǐng)”出常量池。常量池中的其他類(接口)、方法、字段的符號(hào)引用也與此類似。
3.3 無(wú)用的類回收
判定一個(gè)常量是否是“廢棄常量”比較簡(jiǎn)單,而要判定一個(gè)類是否是“無(wú)用的類”的條件則相對(duì)苛刻許多。類需要同時(shí)滿足下面3個(gè)條件才能算是“無(wú)用的類”:
該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例。
加載該類的ClassLoader已經(jīng)被回收。
該類對(duì)應(yīng)的java.lang.Class 對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無(wú)用類進(jìn)行回收,這里說(shuō)的僅僅是“可以”,而不是和對(duì)象一樣,不使用了就必然會(huì)回收。是否對(duì)類進(jìn)行回收,HotSpot虛擬機(jī)提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看類的加載和卸載信息。
在大量使用反射、動(dòng)態(tài)代理、CGLib等bytecode框架的場(chǎng)景,以及動(dòng)態(tài)生成JSP和OSGi這類頻繁自定義ClassLoader的場(chǎng)景都需要虛擬機(jī)具備類卸載的功能,以保證永久代不會(huì)溢出。
總結(jié)
- 這篇文章主要講了JVM中垃圾收集器在堆上進(jìn)行內(nèi)存回收涉及到的內(nèi)容:可達(dá)性分析算法判斷對(duì)象是否無(wú)用、為對(duì)象設(shè)置不同引用類型來(lái)讓對(duì)象的回收時(shí)機(jī)與堆內(nèi)存是否充足相聯(lián)系起來(lái)、對(duì)象的finalize()方法可以在被回收前做一些事情;內(nèi)存回收涉及到的相關(guān)算法,JVM的堆使用了分代收集算法(新生代使用復(fù)制算法、老年代使用標(biāo)記-清除算法)。
- 并講了方法區(qū)中內(nèi)存回收:廢棄常量、無(wú)用類。
- 下一篇:講完內(nèi)存回收相關(guān)知識(shí),下一節(jié)講內(nèi)存分配。堆中的對(duì)象是如何分配的,堆中不同的區(qū)(新生代、老年代)中的對(duì)象怎么分配內(nèi)存的?。
參考文章:
GC在堆和方法區(qū)的內(nèi)存回收
JVM方法區(qū)內(nèi)存回收


