前言
在介紹了對象的創(chuàng)建熄赡、定位以后姜挺,具體的使用則需要根據(jù)不同的業(yè)務(wù)邏輯來確定,這部分是比較自由的彼硫。但正如前文所說炊豪,Java程序員將內(nèi)存的管理交托給了JVM。因此此章節(jié)將進(jìn)一步介紹Java垃圾收集器及內(nèi)存分配策略的相關(guān)知識拧篮。
Java語言出來之前词渤,大家都在拼命的寫C或者C++的程序,而此時存在一個很大的矛盾串绩,C++等語言創(chuàng)建對象要不斷的去開辟空間缺虐,不用的時候有需要不斷的去釋放控件,既要寫構(gòu)造函數(shù)礁凡,又要寫析構(gòu)函數(shù)高氮,很多時候都在重復(fù)的allocated,然后不停的~析構(gòu)顷牌。于是剪芍,有人就提出,能不能寫一段程序在實(shí)現(xiàn)這塊功能窟蓝,每次創(chuàng)建罪裹,釋放控件的時候復(fù)用這段代碼,而無需重復(fù)的書寫呢运挫?
1960年 基于MIT的Lisp首先提出了垃圾回收的概念状共,用于處理C語言等不停的析構(gòu)操作,而這時Java還沒有出世呢谁帕!所以實(shí)際上GC并不是Java的專利峡继,GC的歷史遠(yuǎn)遠(yuǎn)大于Java的歷史!
那究竟GC為我們做了什么操作呢雇卷?
1鬓椭、 哪些內(nèi)存需要回收?
2关划、 什么時候回收?
3翘瓮、 如何回收贮折?
這時候有人就會疑惑了,既然GC已經(jīng)為我們解決了這個矛盾资盅,我們還需要學(xué)習(xí)GC么调榄?當(dāng)然當(dāng)然是肯定的踊赠,那究竟什么時候我們還需要用到的呢?
1每庆、 排查內(nèi)存溢出
2筐带、 排查內(nèi)存泄漏
3、 性能調(diào)優(yōu)缤灵,排查并發(fā)瓶頸
因此這就是學(xué)習(xí)和了解GC機(jī)制的最主要原因伦籍。
一、哪些內(nèi)存需要回收腮出?
針對這個問題帖鸦,我們先要明確一點(diǎn),JVM重點(diǎn)回收的是內(nèi)存共享的區(qū)域胚嘲,也就是堆和方法區(qū)作儿,因?yàn)槌绦蛴嫈?shù)器、虛擬機(jī)棧馋劈、本地方法棧這三個區(qū)域都是線程私有的攻锰,因而隨線程而生,隨線程而滅妓雾;棧中的棧幀隨著方法的進(jìn)入和退出有條不紊地執(zhí)行出棧和入棧操作娶吞。每個棧幀分配多少內(nèi)存基本上以及在類結(jié)構(gòu)確定下來時,便已知曉君珠。因此隨著方法的結(jié)束寝志,這些棧幀所分配的內(nèi)存自然而然被回收了。
而Java堆和方法區(qū)則不一樣策添,一個接口中的多個實(shí)現(xiàn)類需要的內(nèi)存可能不一樣材部,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序運(yùn)行期間才能知道創(chuàng)建哪些對象唯竹,這部分內(nèi)存分配和回收都是動態(tài)的乐导,垃圾收集器所關(guān)注的正是這部分內(nèi)存。
二浸颓、什么時候回收
我們知道物臂,GC主要處理的是對象的回收操作,那么什么時候會觸發(fā)一個對象的回收的呢产上?
1棵磷、 對象沒有引用
2、 作用域發(fā)生未捕獲異常
3晋涣、 程序在作用域正常執(zhí)行完畢
4仪媒、 程序執(zhí)行了System.exit()
5、 程序發(fā)生意外終止(被殺進(jìn)程等)
那么谢鹊,緊接而來的問題便是算吩,如何確定對象已經(jīng)“死去”以達(dá)到被回收的條件留凭?
這里便需要提及的兩種判定對象是否“死去”的方法:
1、 引用計數(shù)算法
在JDK1.2之前偎巢,使用的是引用計數(shù)器算法蔼夜,即當(dāng)這個類被加載到內(nèi)存以后,就會產(chǎn)生方法區(qū)压昼,堆棧求冷、程序計數(shù)器等一系列信息,當(dāng)創(chuàng)建對象的時候巢音,為這個對象在堆椬窬耄空間中分配對象,同時會產(chǎn)生一個引用計數(shù)器官撼,同時引用計數(shù)器+1梧躺,當(dāng)有新的引用的時候,引用計數(shù)器繼續(xù)+1傲绣,而當(dāng)其中一個引用銷毀的時候掠哥,引用計數(shù)器-1,當(dāng)引用計數(shù)器被減為零的時候秃诵,標(biāo)志著這個對象已經(jīng)沒有引用了续搀,可以回收了!這種算法在JDK1.2之前的版本被廣泛使用菠净,但是隨著業(yè)務(wù)的發(fā)展禁舷,很快出現(xiàn)了一個問題
當(dāng)我們的代碼出現(xiàn)下面的情形時,該算法將無法適應(yīng)
a) ObjA.obj = ObjB
b) ObjB.obj = ObjA
這樣的代碼會產(chǎn)生如下引用情形 objA指向objB毅往,而objB又指向objA牵咙,這樣當(dāng)其他所有的引用都消失了之后,objA和objB還有一個相互的引用攀唯,也就是說兩個對象的引用計數(shù)器各為1洁桌,而實(shí)際上這兩個對象都已經(jīng)沒有額外的引用,已經(jīng)是垃圾了侯嘀。
2另凌、可達(dá)性分析算法
根搜索算法是從離散數(shù)學(xué)中的圖論引入的,程序把所有的引用關(guān)系看作一張圖戒幔,從一個節(jié)點(diǎn)GC ROOT開始吠谢,尋找對應(yīng)的引用節(jié)點(diǎn),找到這個節(jié)點(diǎn)以后诗茎,繼續(xù)尋找這個節(jié)點(diǎn)的引用節(jié)點(diǎn)囊卜,當(dāng)所有的引用節(jié)點(diǎn)尋找完畢之后,剩余的節(jié)點(diǎn)則被認(rèn)為是沒有被引用到的節(jié)點(diǎn)错沃,即無用的節(jié)點(diǎn)栅组。
目前java中可作為GC Root的對象有
1、 虛擬機(jī)棧中引用的對象(本地變量表)
2枢析、 方法區(qū)中靜態(tài)屬性引用的對象
3玉掸、 方法區(qū)中常量引用的對象
4、 本地方法棧中引用的對象(Native對象)
說了這么多醒叁,其實(shí)我們可以看到司浪,所有的垃圾回收機(jī)制都是和引用相關(guān)的,那我們來具體的來看一下引用的分類把沼,到底有哪些類型的引用啊易?每種引用都是做什么的呢?
Java中存在四種引用饮睬,每種引用如下:
1租谈、 強(qiáng)引用
只要引用存在,垃圾回收器永遠(yuǎn)不會回收
Object obj = new Object();
//可直接通過obj取得對應(yīng)的對象 如obj.equels(new Object());
而這樣 obj對象對后面new Object的一個強(qiáng)引用捆愁,只有當(dāng)obj這個引用被釋放之后割去,對象才會被釋放掉,這也是我們經(jīng)常所用到的編碼形式昼丑。
2呻逆、 軟引用
非必須引用,內(nèi)存溢出之前進(jìn)行回收菩帝,可以通過以下代碼實(shí)現(xiàn)
????Object obj = new Object();
????SoftReference sf = new SoftReference(obj);
????obj = null;
????sf.get();//有時候會返回null
這時候sf是對obj的一個軟引用咖城,通過sf.get()方法可以取到這個對象,當(dāng)然呼奢,當(dāng)這個對象被標(biāo)記為需要回收的對象時宜雀,則返回null;
3控妻、 弱引用
第二次垃圾回收時回收州袒,可以通過如下代碼實(shí)現(xiàn)
????Object obj = new Object();
????WeakReference wf = new WeakReference(obj);
????obj = null;
????wf.get();//有時候會返回null
????wf.isEnQueued();//返回是否被垃圾回收器標(biāo)記為即將回收的垃圾
弱引用是在第二次垃圾回收時回收,短時間內(nèi)通過弱引用取對應(yīng)的數(shù)據(jù)弓候,可以取到郎哭,當(dāng)執(zhí)行過第二次垃圾回收時,將返回null菇存。
弱引用主要用于監(jiān)控對象是否已經(jīng)被垃圾回收器標(biāo)記為即將回收的垃圾夸研,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器
4、 虛引用(幽靈/幻影引用)
垃圾回收時回收依鸥,無法通過引用取到對象值亥至,可以通過如下代碼實(shí)現(xiàn)
????Object obj = new Object();
????PhantomReference pf = new PhantomReference(obj);
????obj=null;
????pf.get();//永遠(yuǎn)返回null
????pf.isEnQueued();//返回從內(nèi)存中已經(jīng)刪除
虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠(yuǎn)獲取到的數(shù)據(jù)為null侣夷,因此也被成為幽靈引用窿给。
虛引用主要用于檢測對象是否已經(jīng)從內(nèi)存中刪除。
在上文中已經(jīng)提到了帆赢,我們的對象在內(nèi)存中會被劃分為5塊區(qū)域茶敏,而每塊數(shù)據(jù)的回收比例是不同的壤靶,根據(jù)IBM的統(tǒng)計,數(shù)據(jù)如下圖所示:
我們知道惊搏,方法區(qū)主要存放類與類之間關(guān)系的數(shù)據(jù)贮乳,而這部分?jǐn)?shù)據(jù)被加載到內(nèi)存之后,基本上是不會發(fā)生變更的
Java堆中的數(shù)據(jù)基本上是朝生夕死的恬惯,我們用完之后要馬上回收的向拆,而Java棧和本地方法棧中的數(shù)據(jù),因?yàn)橛泻筮M(jìn)先出的原則酪耳,當(dāng)我取下面的數(shù)據(jù)之前浓恳,必須要把棧頂?shù)脑爻鰲#虼嘶厥章士烧J(rèn)為是100%葡兑;而程序計數(shù)器我們前面也已經(jīng)提到奖蔓,主要用戶記錄線程執(zhí)行的行號等一些信息,這塊區(qū)域也是被認(rèn)為是唯一一塊不會內(nèi)存溢出的區(qū)域讹堤。在SunHostSpot的虛擬機(jī)中吆鹤,對于程序計數(shù)器是不回收的,而方法區(qū)的數(shù)據(jù)因?yàn)榛厥章史浅P≈奘兀杀居直容^高疑务,一般認(rèn)為是“性價比”非常差的,所以Sun自己的虛擬機(jī)HotSpot中是不回收的梗醇!但是在現(xiàn)在高性能分布式J2EE的系統(tǒng)中知允,我們大量用到了反射、動態(tài)代理叙谨、CGLIB温鸽、JSP和OSGI等,這些類頻繁的調(diào)用自定義類加載器手负,都需要動態(tài)的加載和卸載了涤垫,以保證永久帶不會溢出,他們通過自定義的類加載器進(jìn)行了各項(xiàng)操作竟终,因此在實(shí)際的應(yīng)用開發(fā)中蝠猬,類也是被經(jīng)常加載和卸載的,方法區(qū)也是會被回收的统捶!但是方法區(qū)的回收條件非秤苈苛刻柄粹,只有同時滿足以下三個條件才會被回收!
1匆绣、所有實(shí)例被回收
2驻右、加載該類的ClassLoader被回收
3、Class對象無法通過任何途徑訪問(包括反射)
三犬绒、如何回收旺入?
好了,我們現(xiàn)在切入正題凯力,Java1.2之前主要通過引用計數(shù)器來標(biāo)記是否需要垃圾回收,而1.2之后都使用根搜索算法來收集垃圾礼华,而收集后的垃圾是通過什么算法來回收的呢咐鹤?
1、 標(biāo)記-清除算法
2圣絮、 復(fù)制算法
3祈惶、 標(biāo)記-整理算法
我們來逐一過一下
1、 標(biāo)記-清除算法
? ?標(biāo)記-清除算法采用從根集合進(jìn)行掃描扮匠,對存活的對象對象標(biāo)記捧请,標(biāo)記完畢后,再掃描整個空間中未被標(biāo)記的對象棒搜,進(jìn)行回收疹蛉,如上圖所示。
標(biāo)記-清除算法不需要進(jìn)行對象的移動力麸,并且僅對不存活的對象進(jìn)行處理可款,在存活對象比較多的情況下極為高效,但由于標(biāo)記-清除算法直接回收不存活的對象克蚂,因此會造成內(nèi)存碎片闺鲸!
2、 復(fù)制算法
????復(fù)制算法采用從根集合掃描埃叭,并將存活對象復(fù)制到一塊新的摸恍,沒有使用過的空間中,這種算法當(dāng)控件存活的對象比較少時赤屋,極為高效立镶,但是帶來的成本是需要一塊內(nèi)存交換空間用于進(jìn)行對象的移動。也就是我們前面提到的s0 s1等空間益缎。
3谜慌、 標(biāo)記-整理算法
標(biāo)記 -整理算法采用標(biāo)記-清除算法一樣的方式進(jìn)行對象的標(biāo)記,但在清除時不同莺奔,在回收不存活的對象占用的空間后欣范,會將所有的存活對象往左端空閑空間移動变泄,并更新對應(yīng)的指針。標(biāo)記-整理算法是在標(biāo)記-清除算法的基礎(chǔ)上恼琼,又進(jìn)行了對象的移動妨蛹,因此成本更高,但是卻解決了內(nèi)存碎片的問題晴竞。
我們知道蛙卤,JVM為了優(yōu)化內(nèi)存的回收,進(jìn)行了分代回收的方式噩死,對于新生代內(nèi)存的回收(minor GC)主要采用復(fù)制算法颤难,下圖展示了minor GC的執(zhí)行過程。
????對于新生代和舊生代已维, JVM可使用很多種垃圾回收器進(jìn)行垃圾回收行嗤,下圖展示了不同生代不通垃圾回收器,其中兩個回收器之間有連線表示這兩個回收器可以同時使用垛耳。
而這些垃圾回收器又分為串行回收方式栅屏、并行回收方式合并發(fā)回收方式執(zhí)行,分別運(yùn)用于不同的場景堂鲜。如下圖所示
????下面我們來逐一介紹一下每個垃圾回收器栈雳。
Serial收集器
????收集器是歷史最悠久的一個回收器,JDK1.3之前廣泛使用這個收集器缔莲,目前也是ClientVM下 ServerVM 4核4GB以下機(jī)器的默認(rèn)垃圾回收器哥纫。串行收集器并不是只能使用一個CPU進(jìn)行收集,而是當(dāng)JVM需要進(jìn)行垃圾回收的時候酌予,需要中斷所有的用戶線程磺箕,知道它回收結(jié)束為止,因此又號稱“Stop The World” 的垃圾回收器抛虫。注意松靡,JVM中文名稱為java虛擬機(jī),因此它就像一臺虛擬的電腦一樣在工作建椰,而其中的每一個線程就被認(rèn)為是JVM的一個處理器雕欺,因此大家看到圖中的CPU0、CPU1實(shí)際為用戶的線程棉姐,而不是真正機(jī)器的CPU屠列,大家不要誤解哦。
????串行回收方式適合低端機(jī)器伞矩,是Client模式下的默認(rèn)收集器笛洛,對CPU和內(nèi)存的消耗不高,適合用戶交互比較少乃坤,后臺任務(wù)較多的系統(tǒng)苛让。
Serial收集器默認(rèn)新舊生代的回收器搭配為Serial+ SerialOld
????1沟蔑、單線程收集器
????2、進(jìn)行垃圾回收時必須暫停用戶所有的工作線程狱杰。
Serial收集器對于運(yùn)行在Client模式下的虛擬機(jī)來說是一個很棒的選擇
ParNew收集器
????ParNew收集器其實(shí)就是多線程版本的Serial收集器瘦材,其運(yùn)行示意圖如下
同樣有 Stop The World的問題,他是多CPU模式下的首選回收器(該回收器在單CPU的環(huán)境下回收效率遠(yuǎn)遠(yuǎn)低于Serial收集器仿畸,所以一定要注意場景哦)食棕,也是Server模式下的默認(rèn)收集器。
1错沽、Serial的多線程版本
2簿晓、是運(yùn)行在Server模式下的首選新生代收集器,只有它可以與CMS收集器配合工作甥捺。通過-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)
Parallel Scavenge收集器
ParallelScavenge又被稱為是吞吐量優(yōu)先的收集器抢蚀,器運(yùn)行示意圖如下
????所提到的吞吐量=程序運(yùn)行時間/(JVM執(zhí)行回收的時間+程序運(yùn)行時間),假設(shè)程序運(yùn)行了100分鐘,JVM的垃圾回收占用1分鐘镰禾,那么吞吐量就是99%。在當(dāng)今網(wǎng)絡(luò)告訴發(fā)達(dá)的今天唱逢,良好的響應(yīng)速度是提升用戶體驗(yàn)的一個重要指標(biāo)吴侦,多核并行云計算的發(fā)展要求程序盡可能的使用CPU和內(nèi)存資源,盡快的計算出最終結(jié)果坞古,因此在交互不多的云端备韧,比較適合使用該回收器。
1痪枫、是并行的多線程收集器
2织堂、目的是達(dá)到一個可控制的吞吐量(Throughput)
3、-XX:MaxGCPauseMillis :最大垃圾手機(jī)停頓時間
-XX:GCTimeRatio:設(shè)置吞吐量大小
-XX:+UseAdaptiveSizePolicy 打開后奶陈,虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行狀況收集性能監(jiān)控信息易阳。
Serial Old 收集器
SerialOld是舊生代Client模式下的默認(rèn)收集器,單線程執(zhí)行吃粒;在JDK1.6之前也是ParallelScvenge回收新生代模式下舊生代的默認(rèn)收集器潦俺,同時也是并發(fā)收集器CMS回收失敗后的備用收集器。其運(yùn)行示意圖如下
?Serial的老年代版本,采用“標(biāo)記-整理”算法
Parallel Old 收集器
ParallelOld是老生代并行收集器的一種徐勃,使用標(biāo)記整理算法事示、是老生代吞吐量優(yōu)先的一個收集器。這個收集器是JDK1.6之后剛引入的一款收集器僻肖,我們看之前那個圖之間的關(guān)聯(lián)關(guān)系可以看到肖爵,早期沒有ParallelOld之前,吞吐量優(yōu)先的收集器老生代只能使用串行回收收集器臀脏,大大的拖累了吞吐量優(yōu)先的性能劝堪,自從JDK1.6之后冀自,才能真正做到較高效率的吞吐量優(yōu)先。其運(yùn)行示意圖如下幅聘。
Parallel Scavenge收集器的老年代版本,采用“標(biāo)記-整理”算法
1凡纳、在注重吞吐量以及CPU資源敏感結(jié)合,可以考慮Parallel Scavenge加Parallel Old收集器
CMS(Concurrent Mark Sweep)收集器
CMS又稱響應(yīng)時間優(yōu)先(最短回收停頓)的回收器帝蒿,使用并發(fā)模式回收垃圾荐糜,使用標(biāo)記-清除算法,CMS對CPU是非常敏感的葛超,它的回收線程數(shù)=(CPU+3)/4暴氏,因此當(dāng)CPU是2核的實(shí)惠,回收線程將占用的CPU資源的50%绣张,而當(dāng)CPU核心數(shù)為4時僅占用25%答渔。他的運(yùn)行示意圖如下
CMS收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。使用與B/S系統(tǒng)服務(wù)器上侥涵。是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的沼撕。
運(yùn)作過程:
(1)初始標(biāo)記
(2)并發(fā)標(biāo)記
(3)重新標(biāo)記
(4)并發(fā)清除
????在初始標(biāo)記的時候,需要中斷所有用戶線程芜飘,在并發(fā)標(biāo)記階段务豺,用戶線程和標(biāo)記線程并發(fā)執(zhí)行,而在這個過程中嗦明,隨著內(nèi)存引用關(guān)系的變化笼沥,可能會發(fā)生原來標(biāo)記的對象被釋放,進(jìn)而引發(fā)新的垃圾娶牌,因此可能會產(chǎn)生一系列的浮動垃圾奔浅,不能被回收。
CMS 為了確保能夠掃描到所有的對象诗良,避免在Initial Marking 中還有未標(biāo)識到的對象汹桦,采用的方法為找到標(biāo)記了的對象,并將這些對象放入Stack 中累榜,掃描時尋找此對象依賴的對象营勤,如果依賴的對象的地址在其之前,則將此對象進(jìn)行標(biāo)記壹罚,并同時放入Stack 中葛作,如依賴的對象地址在其之后,則僅標(biāo)記該對象猖凛。
在進(jìn)行Concurrent Marking 時minor GC 也可能會同時進(jìn)行赂蠢,這個時候很容易造成舊生代對象引用關(guān)系改變,CMS 為了應(yīng)對這樣的并發(fā)現(xiàn)象辨泳,提供了一個Mod Union Table 來進(jìn)行記錄虱岂,在這個Mod Union Table中記錄每次minor GC 后修改了的Card 的信息玖院。這也是ParallelScavenge不能和CMS一起使用的原因。
CMS產(chǎn)生浮動垃圾的情況請見如下示意圖第岖。
????在運(yùn)行回收過后难菌,c就變成了浮動垃圾。
????由于CMS會產(chǎn)生浮動垃圾蔑滓,當(dāng)回收過后郊酒,浮動垃圾如果產(chǎn)生過多,同時因?yàn)槭褂脴?biāo)記-清除算法會產(chǎn)生碎片键袱,可能會導(dǎo)致回收過后的連續(xù)空間仍然不能容納新生代移動過來或者新創(chuàng)建的大資源燎窘,因此會導(dǎo)致CMS回收失敗,進(jìn)而觸發(fā)另外一次FULL GC蹄咖,而這時候則采用SerialOld進(jìn)行二次回收褐健。
????同時CMS因?yàn)榭赡墚a(chǎn)生浮動垃圾,而CMS在執(zhí)行回收的同時新生代也有可能在進(jìn)行回收操作澜汤,為了保證舊生代能夠存放新生代轉(zhuǎn)移過來的數(shù)據(jù)蚜迅,CMS在舊生代內(nèi)存到達(dá)全部容量的68%就觸發(fā)了CMS的回收!
主要的三個缺點(diǎn):
(1)CMS收集器對CPU資源非常敏感俊抵,因?yàn)槭遣⑿袠?biāo)記和并行清除的慢叨,多CPU性能存在影響。
(2)CMS收集器無法處理浮動垃圾,因?yàn)橛脩艟€程還在運(yùn)行务蝠,運(yùn)行過程中還會產(chǎn)生垃圾
(3)空間碎片,因?yàn)椴捎玫氖恰皹?biāo)記-清除”算法
G1收集器
G1與CMS比較的兩個改進(jìn)的地方:
(1)使用“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器烛缔,不會產(chǎn)生空間碎片
(2)非常精準(zhǔn)的控制停頓馏段,既能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不超過N毫秒践瓷。
(3)G1可以實(shí)現(xiàn)基本不犧牲吞吐量的情況下完成低停頓的內(nèi)存回收院喜。
(4)G1將新生代和老年代劃分為多個大小固定的獨(dú)立區(qū)域(Region),并且跟蹤這些區(qū)域里面的垃圾堆積程度晕翠,在后臺維護(hù)一個優(yōu)先列表喷舀。