知識要點:
垃圾回收要點知識
垃圾回收算法
垃圾回收器
垃圾回收機制
GC所關心的東西:“這塊數(shù)據(jù)是不是一個指針”
GC所關心最重要的幾件事情:
哪些內(nèi)存要回收?
什么時候回收伐庭?
如何回收?
垃圾回收要點知識
引用計數(shù)
給對象添加一個引用計數(shù)器分冈。每當有一個地方引用這個對象圾另,這個計數(shù)器就加1;每當引用失效雕沉,這個計數(shù)器就減1集乔;當計數(shù)器為0的時候就代表該對象不能再被使用。
public class ReferenceCountingGC{
public Object instance = null;
private static final int_1MB = 1024*1024;
/**
* 這個成員屬性的唯一意義就是占點內(nèi)存坡椒,以便能在GC日志中看清楚是否被回收過
*/
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;
// 假設在這行發(fā)生GC扰路,objA和objB是否能被回收?
System.gc()倔叼;
}
}
上面的例子汗唱,兩個對象雖然再無任何引用,實際上這兩個對象已經(jīng)不可能再被訪問丈攒,但是它們因為互相引用著對方哩罪,導致它們的引用計數(shù)都不為0,于是引用計數(shù)算法無法通知GC收集器回收它們巡验。
可達性分析
JVM中對內(nèi)存進行回收時际插,需要判斷對象是否仍在使用中,可以通過GC Roots Tracing辨別显设。
通過一系列名為”GCRoots”的對象作為起始點框弛,從這個節(jié)點向下搜索,搜索走過的路徑稱為ReferenceChain捕捂,當一個對象到GCRoots沒有任何ReferenceChain相連時瑟枫,(圖論:這個對象不可到達)斗搞,則證明這個對象不可用。
在Java語言中慷妙,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象榜旦。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象景殷。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
四種引用
在JDK 1.2之后溅呢,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)猿挚、 軟引用(Soft Reference)咐旧、 弱引用(Weak Reference)、 虛引用(Phantom Reference)4種绩蜻,這4種引用強度依次逐漸減弱铣墨。
強引用
在程序代碼之中普遍存在類似“Object obj = new Object()”這類的引用,只要強引用還存在办绝,垃圾收集器永遠不會回收掉被引用的對象伊约。
強引用有引用變量指向時永遠不會被垃圾回收,JVM寧愿拋出OutOfMemory錯誤也不會回收這種對象孕蝉。如果想中斷強引用和某個對象之間的關聯(lián)屡律,可以顯示地將引用賦值為null。
軟引用
用來描述一些還有用但并非必需的對象降淮。對于軟引用關聯(lián)著的對象超埋,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收佳鳖。如果這次回收沒有足夠的內(nèi)存霍殴,才會拋出內(nèi)存溢出異常。
public class SoftRef {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
SoftReference aSoftRef = new SoftReference(object);
//當結束object對這個MyObject實例的強引用系吩,MyObject對象成為了軟引用對象
object = null;
//重新獲得對該實例的強引用来庭。而回收之后,調(diào)用get()方法就只能得到null了
Object anotherRef = (Object) aSoftRef.get();
System.out.println(anotherRef == null);
}
}
弱引用
也是用來描述非必需對象的穿挨,但是它的強度比軟引用更弱一些月弛,被弱引用關聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時絮蒿,無論當前內(nèi)存是否足夠尊搬,都會回收掉被弱引用關聯(lián)的對象。
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "[name:" + name + ",age:" + age + "]";
}
}
public class WeakRef {
public static void main(String[] args) {
WeakReference<Person> reference = new WeakReference<Person>(new Person("zhouqian", 20));
System.out.println(reference.get());
//通知JVM回收資源
System.gc();
System.out.println(reference.get());
}
}
虛引用
也稱為幽靈引用或者幻影引用土涝,它是最弱的一種引用關系。一個對象是否有虛引用的存在幌墓,完全不會對其生存時間構成影響但壮,也無法通過虛引用來取得一個對象實例冀泻。為一個對象設置虛引用關聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。
// 幽靈引用
public class PhanReference {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> pr = new PhantomReference<>(new String("hello"), queue);
System.out.println(pr.get());
}
}
垃圾回收算法
標記清除
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法蜡饵。該算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象弹渔,在標記完成后統(tǒng)一回收所有被標記的對象。
該算法有兩個主要不足:
- 一個是效率問題溯祸,標記和清除的效率都不高肢专;
- 另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片焦辅。
標記整理
標記-整理(Mark-Compact)算法博杖,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理筷登,而是讓所有存活的對象都向一端移動剃根,然后直接清理掉端邊界以外的內(nèi)存
復制
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊前方。當這一塊的內(nèi)存用完了狈醉,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉惠险。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收苗傅,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針班巩,按順序分配內(nèi)存即可金吗,實現(xiàn)簡單,運行高效趣竣。
缺點:內(nèi)存縮小為了原來的一半摇庙,內(nèi)存利用率太低
HotSpot算法
枚舉根節(jié)點
從可達性分析中從GC Roots節(jié)點找引用鏈這個操作為例,可作為GC Roots的節(jié)點主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中遥缕,現(xiàn)在很多應用僅僅方法區(qū)就有數(shù)百兆卫袒,如果要逐個檢查這里面的引用,那么必然會消耗很多時間单匣。
另外夕凝,可達性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項分析工作必須分析期間整個執(zhí)行系統(tǒng)看起來就像被凍結在某個時間點上户秤,不可以出現(xiàn)分析過程中對象引用關系還在不斷變化的情況码秉,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執(zhí)行線程(Sun將這個稱為“Stop The World”)的其中一個重要原因鸡号,即使是在號稱(幾乎)不會發(fā)生停頓的CMS收集器中转砖,枚舉根節(jié)點時也是必須要停頓的。
GC分類
保守式GC
JVM選擇不記錄任何類型的數(shù)據(jù),那么它就無法區(qū)分內(nèi)存里某個位置上的數(shù)據(jù)到底應該解讀為引用類型還是整型還是別的什么府蔗。這種條件下晋控,實現(xiàn)出來的GC就會是“保守式GC(conservative GC)”。在進行GC的時候姓赤,JVM開始從一些已知位置開始掃描內(nèi)存赡译,掃描的時候每看到一個數(shù)字就判斷它“像不像是一個指向GC堆中的指針”。
半保守式GC
JVM可以選擇在對象上記錄類型信息不铆。這樣的話蝌焚,掃描到GC堆內(nèi)的對象時因為對象帶有足夠類型信息了,JVM就能夠判斷出在該對象內(nèi)什么位置的數(shù)據(jù)是引用類型了誓斥。這種是“半保守式GC”只洒,也稱為“根上保守(conservative with respect to the roots)”。
為了支持半保守式GC岖食,運行時需要在對象上帶有足夠的元數(shù)據(jù)红碑。如果是JVM的話,這些數(shù)據(jù)可能在類加載器或者對象模型的模塊里計算得到泡垃,但不需要JIT編譯器的特別支持析珊。
準確式GC
從外部記錄下類型信息,存成映射表∶镅ǎ現(xiàn)在三種主流的高性能JVM實現(xiàn)忠寻,HotSpot、JRockit和J9都是這樣做的存和。其中奕剃,HotSpot把這樣的數(shù)據(jù)結構叫做OopMap。
OopMap
在HotSpot中捐腿,對象的類型信息里有記錄自己的OopMap纵朋,記錄了在該類型的對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)。所以從對象開始向外的掃描可以是準確的茄袖;這些數(shù)據(jù)是在類加載過程中計算得到的操软。
安全點
在OopMap的協(xié)助下,HotSpot可以快速且準確地完成GC Roots枚舉宪祥,但一個很現(xiàn)實的問題隨之而來:可能導致引用關系變化聂薪,或者說OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應的OopMap蝗羊,那將會需要大量的額外空間藏澳,這樣GC的空間成本將會變得很高。
安全點太多耀找,GC 過于頻繁翔悠,增大運行時負荷;安全點太少,GC 等待時間太長凉驻。一般會在如下幾個位置選擇安全點:
①循環(huán)的末尾
②方法臨返回前
③調(diào)用方法之后
④拋異常的位置
安全區(qū)域
假如線程處于Sleep或者Blocked狀態(tài)腻要,這時候線程無法響應JVM的中斷請求复罐,也就無法到達Safepoint的地方中斷掛起涝登。對于這種情況,就需要安全區(qū)域(Safe Region)來解決效诅。
安全區(qū)域是指在一段代碼片段之中胀滚,引用關系不會發(fā)生變化。在這個區(qū)域中的任意地方開始GC都是安全的乱投。 也可以把Safe Region看做是Safepoint的擴展咽笼。
JDK垃圾回收器
Serial
單線程的收集器,它的“單線程”的意義并不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作戚炫,最重要的是在它進行垃圾收集時剑刑,必須暫停其他所有的工作線程,直到它收集結束
- 開啟參數(shù):-XX:+UseSerialGC
- 適用場景:用戶的桌面應用場景
ParNew
Serial收集器的多線程版本双肤,除了使用多條線程進行垃圾收集之外施掏,其余行為包括Serial收集器可用的所有控制參數(shù)、 收集算法茅糜、 Stop The World七芭、 對象分配規(guī)則、 回收策略等都與Serial收集器完全一樣
- 開啟參數(shù):-XX:+UseParNewGC
- 適用場景:Server首選的新生代收集器
Parallel Scavenge
Parallel Scavenge收集器是一個新生代收集器蔑赘,它也是使用復制算法的收集器狸驳,Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,它的目標是達到一個可控制的吞吐量(Throughput)
吞吐量計算公式:運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
垃圾收集停頓時間越短就越適合需要與用戶交互的程序缩赛,良好的響應速度能提升用戶體驗耙箍,而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務酥馍,主要適合在后臺運算而不需要太多交互的任務辩昆。
Parallel Scavenge提供了兩個參數(shù)用于精確控制吞吐量
- -XX:MaxGCPauseMillis // 最大垃圾收集停頓時間(大于0毫秒數(shù))
- -XX:GCTimeRatio // 吞吐量大小(大于0且小于100的整數(shù)物喷,吞吐量百分比)
- -XX:+UseAdaptiveSizePolicy // 內(nèi)存調(diào)優(yōu)委托給虛擬機管理卤材。當這個參數(shù)打開之后,就不需要手工指定新生代的大小峦失、Eden與Survivor區(qū)的比例扇丛、 晉升老年代對象年齡等細節(jié)參數(shù)了,虛擬機會根據(jù)當前系統(tǒng)的運行情況收集性能監(jiān)控信息尉辑,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量
- 開啟參數(shù):-XX:+UseParallelGC
- 適用場景:后臺計算不需要太多交互
Serial Old
Serial Old是Serial收集器的老年代版本帆精,它同樣是一個單線程收集器,使用“標記-整理”算法。 這個收集器的主要意義也是在于給Client模式下的虛擬機使用
- 適用場景:用戶的桌面應用場景
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本卓练,使用多線程和“標記-整理”算法隘蝎。這個收集器從JDK 1.6中才開始提供的。
- 開啟參數(shù):-XX:+UseParallelOldGC
- 適用場景:用戶的桌面應用場景
Concurrent Mark Sweep
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器襟企。CMS收集器是基于“標記—清除”算法實現(xiàn)的嘱么。整個過程分為4個步驟
- 初始標記(CMS initial mark)
- 并發(fā)標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
缺點
- CMS收集器對CPU資源非常敏感。
- CMS收集器無法處理浮動垃圾(Floating Garbage)顽悼,可能出現(xiàn)“Concurrent Mode Failure”失敗而導致另一次Full GC的產(chǎn)生曼振。
- CMS是一款基于“標記—清除”算法實現(xiàn)的收集器**,這意味著收集結束時會有大量空間碎片產(chǎn)生蔚龙。
- 開啟參數(shù):-XX:+UseConcMarkSweepGC
- 適用場景:互聯(lián)網(wǎng)站或者WEB服務端
G1
G1算法將堆劃分為若干個區(qū)域(Region)冰评,但它仍然屬于分代收集器。
不過木羹,這些區(qū)域的一部分包含新生代甲雅,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間坑填。
老年代也分成很多區(qū)域抛人,G1收集器通過將對象從一個區(qū)域復制到另外一個區(qū)域,完成了清理工作穷遂。這就意味著函匕,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮)蚪黑,這樣也就不會有CMS內(nèi)存碎片問題的存在了盅惜。
G1提供了兩種GC模式(兩種都是Stop The World的)
- Young GC
-
Mixed GC
YoungGC
階段1:根掃描。靜態(tài)和本地對象被掃描
階段2:更新RS(Remembered Set)忌穿。找出老年代到年輕代的引用并更新RS
階段3:處理RS抒寂。檢測從年輕代指向年老代的對象
階段4:對象拷貝÷咏#拷貝存活的對象到survivor/old區(qū)域
階段5:處理引用隊列屈芜。軟引用,弱引用朴译,虛引用處理
Mix GC
①初始標記 該階段僅僅只是標記一下GC Roots能直接關聯(lián)到的對象井佑。
②并發(fā)標記 該階段是從GCRoot開始對堆中對象進行可達性分析,找出存活的對象眠寿,這階段耗時較長躬翁,但可與用戶程序并發(fā)執(zhí)行。
③最終標記 該階段則是為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄盯拱。
④篩選回收 該階段首先對各個Region的回收價值和成本進行排序盒发,根據(jù)用戶所期望的GC停頓時間來制定回收計劃例嘱。因為只回收一部分Region,所以時間是用戶可控制的宁舰,而且停頓用戶線程將大幅提高收集效率拼卵。
- 開啟參數(shù):-XX:+UseG1GC
- 適用場景:服務端應用
垃圾回收機制
垃圾回收機制后續(xù)再補齊!