Java語言出來之前,,大家都在拼命的寫C或者C++的程序,而此時(shí)存在一個(gè)很大的矛盾,,C++等語言創(chuàng)建對象要不斷的去開辟空間,,不用的時(shí)候有需要不斷的去釋放控件,既要寫構(gòu)造函數(shù),,又要寫析構(gòu)函數(shù),,很多時(shí)候都在重復(fù)的allocated,,然后不停的~析構(gòu)。于是,,有人就提出,,能不能寫一段程序在實(shí)現(xiàn)這塊功能,每次創(chuàng)建,,釋放控件的時(shí)候復(fù)用這段代碼,,而無需重復(fù)的書寫呢? 1960年 基于MIT的Lisp首先提出了垃圾回收的概念,,用于處理C語言等不停的析構(gòu)操作,,而這時(shí)Java還沒有出世呢!所以實(shí)際上GC并不是Java的專利,,GC的歷史遠(yuǎn)遠(yuǎn)大于Java的歷史,! 那究竟GC為我們做了什么操作呢,?
這時(shí)候有人就會疑惑了,,既然GC已經(jīng)為我們解決了這個(gè)矛盾,,我們還需要學(xué)習(xí)GC么?當(dāng)然當(dāng)然是肯定的,,那究竟什么時(shí)候我們還需要用到的呢,?
我們知道,,GC主要處理的是對象的回收操作,,那么什么時(shí)候會觸發(fā)一個(gè)對象的回收的呢? 1,、 對象沒有引用 2,、 作用域發(fā)生未捕獲異常 3、 程序在作用域正常執(zhí)行完畢 4,、 程序執(zhí)行了System.exit() 5,、 程序發(fā)生意外終止(被殺進(jìn)程等) 其實(shí),我們最容易想到的就是當(dāng)對象沒有引用的時(shí)候會將這個(gè)對象標(biāo)記為可回收對象,那么現(xiàn)在就有一個(gè)問題,,是不是這個(gè)對象被賦值為null以后就一定被標(biāo)記為可回收對象了呢,?我們來看一個(gè)例子:
程序的運(yùn)行結(jié)果回事什么樣子的呢,? 我們來看這段代碼
1,、 當(dāng)程序執(zhí)行第一行是,因?yàn)檫@個(gè)對象沒有值,,結(jié)果肯定是null 2,、 程序第二行給該對象賦值為新開辟的一個(gè)對象 3、 第三行打印的時(shí)候,,肯定是第二行對象的hash代碼 4,、 第四行將該對象重新置為null 5、 第五行觸發(fā)GC 6,、 為了保證GC能夠順利執(zhí)行完畢,,第六行等待100毫秒 7、 第七行打印對應(yīng)的值,,回事null么,?一定會是null么? 我們來看一下對應(yīng)的運(yùn)行結(jié)果
本例中打印了 GC的日志,,讓我們看的更清晰一點(diǎn),,我們很清晰的看出,最后一句打印的不是null,,并且子啊之前,,還出現(xiàn)了逃逸的字樣。說明這個(gè)對象逃逸了,,在垃圾回收之前逃逸了,,我們再來看這個(gè)pojo的寫法,就會發(fā)現(xiàn),,我們重寫了方法finalize,,而這個(gè)方法就相當(dāng)于C++中的析構(gòu)方法,在GC回收之前,會先調(diào)用一次這個(gè)方法,,而這個(gè)方法又將this指針指向他自己,,因此得以成功逃逸!可見,,并不是這個(gè)對象被賦值為null之后就一定被標(biāo)記為可回收,,有可能會發(fā)生逃逸!
下面我們來看一下幾種垃圾收集算法 1,、 在JDK1.2之前,,使用的是引用計(jì)數(shù)器算法,即當(dāng)這個(gè)類被加載到內(nèi)存以后,,就會產(chǎn)生方法區(qū),,堆棧、程序計(jì)數(shù)器等一系列信息,,當(dāng)創(chuàng)建對象的時(shí)候,,為這個(gè)對象在堆棧空間中分配對象,,同時(shí)會產(chǎn)生一個(gè)引用計(jì)數(shù)器,,同時(shí)引用計(jì)數(shù)器+1,當(dāng)有新的引用的時(shí)候,,引用計(jì)數(shù)器繼續(xù)+1,,而當(dāng)其中一個(gè)引用銷毀的時(shí)候,引用計(jì)數(shù)器-1,,當(dāng)引用計(jì)數(shù)器被減為零的時(shí)候,,標(biāo)志著這個(gè)對象已經(jīng)沒有引用了,可以回收了,!這種算法在JDK1.2之前的版本被廣泛使用,,但是隨著業(yè)務(wù)的發(fā)展,很快出現(xiàn)了一個(gè)問題 當(dāng)我們的代碼出現(xiàn)下面的情形時(shí),,該算法將無法適應(yīng) a) ObjA.obj = ObjB b) ObjB.obj - ObjA 這樣的代碼會產(chǎn)生如下引用情形 objA指向objB,,而objB又指向objA,這樣當(dāng)其他所有的引用都消失了之后,,objA和objB還有一個(gè)相互的引用,,也就是說兩個(gè)對象的引用計(jì)數(shù)器各為1,而實(shí)際上這兩個(gè)對象都已經(jīng)沒有額外的引用,,已經(jīng)是垃圾了,。
2、 根搜索算法 根搜索算法是從離散數(shù)學(xué)中的圖論引入的,,程序把所有的引用關(guān)系看作一張圖,,從一個(gè)節(jié)點(diǎn)GC ROOT開始,,尋找對應(yīng)的引用節(jié)點(diǎn),找到這個(gè)節(jié)點(diǎn)以后,,繼續(xù)尋找這個(gè)節(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的一個(gè)強(qiáng)引用,,只有當(dāng)obj這個(gè)引用被釋放之后,對象才會被釋放掉,,這也是我們經(jīng)常所用到的編碼形式,。 2、 軟引用 非必須引用,,內(nèi)存溢出之前進(jìn)行回收,,可以通過以下代碼實(shí)現(xiàn) Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; sf.get();//有時(shí)候會返回null 這時(shí)候sf是對obj的一個(gè)軟引用,通過sf.get()方法可以取到這個(gè)對象,,當(dāng)然,,當(dāng)這個(gè)對象被標(biāo)記為需要回收的對象時(shí),則返回null,; 3,、 弱引用 第二次垃圾回收時(shí)回收,可以通過如下代碼實(shí)現(xiàn) Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; wf.get();//有時(shí)候會返回null wf.isEnQueued();//返回是否被垃圾回收器標(biāo)記為即將回收的垃圾 弱引用是在第二次垃圾回收時(shí)回收,,短時(shí)間內(nèi)通過弱引用取對應(yīng)的數(shù)據(jù),,可以取到,當(dāng)執(zhí)行過第二次垃圾回收時(shí),,將返回null,。 弱引用主要用于監(jiān)控對象是否已經(jīng)被垃圾回收器標(biāo)記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器 4,、 虛引用(幽靈/幻影引用) 垃圾回收時(shí)回收,,無法通過引用取到對象值,可以通過如下代碼實(shí)現(xiàn) Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); obj=null; pf.get();//永遠(yuǎn)返回null pf.isEnQueued();//返回從內(nèi)存中已經(jīng)刪除 虛引用是每次垃圾回收的時(shí)候都會被回收,,通過虛引用的get方法永遠(yuǎn)獲取到的數(shù)據(jù)為null,,因此也被成為幽靈引用。 虛引用主要用于檢測對象是否已經(jīng)從內(nèi)存中刪除,。 在上文中已經(jīng)提到了,,我們的對象在內(nèi)存中會被劃分為5塊區(qū)域,而每塊數(shù)據(jù)的回收比例是不同的,,根據(jù)IBM的統(tǒng)計(jì),,數(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%,;而程序計(jì)數(shù)器我們前面也已經(jīng)提到,,主要用戶記錄線程執(zhí)行的行號等一些信息,這塊區(qū)域也是被認(rèn)為是唯一一塊不會內(nèi)存溢出的區(qū)域,。在SunHostSpot的虛擬機(jī)中,,對于程序計(jì)數(shù)器是不回收的,而方法區(qū)的數(shù)據(jù)因?yàn)榛厥章史浅P?,而成本又比較高,,一般認(rèn)為是“性價(jià)比”非常差的,所以Sun自己的虛擬機(jī)HotSpot中是不回收的,!但是在現(xiàn)在高性能分布式J2EE的系統(tǒng)中,,我們大量用到了反射,、動態(tài)代理、CGLIB,、JSP和OSGI等,,這些類頻繁的調(diào)用自定義類加載器,都需要?jiǎng)討B(tài)的加載和卸載了,,以保證永久帶不會溢出,,他們通過自定義的類加載器進(jìn)行了各項(xiàng)操作,因此在實(shí)際的應(yīng)用開發(fā)中,,類也是被經(jīng)常加載和卸載的,,方法區(qū)也是會被回收的!但是方法區(qū)的回收條件非??量?,只有同時(shí)滿足以下三個(gè)條件才會被回收!
1,、所有實(shí)例被回收 2,、加載該類的ClassLoader被回收 3、Class對象無法通過任何途徑訪問(包括反射) 好了,,我們現(xiàn)在切入正題,,Java1.2之前主要通過引用計(jì)數(shù)器來標(biāo)記是否需要垃圾回收,而1.2之后都使用根搜索算法來收集垃圾,,而收集后的垃圾是通過什么算法來回收的呢,? 1、 標(biāo)記-清除算法 2,、 復(fù)制算法 3、 標(biāo)記-整理算法 我們來逐一過一下 1,、 標(biāo)記-清除算法
標(biāo)記-清除算法采用從根集合進(jìn)行掃描,,對存活的對象對象標(biāo)記,標(biāo)記完畢后,,再掃描整個(gè)空間中未被標(biāo)記的對象,,進(jìn)行回收,如上圖所示,。 標(biāo)記-清除算法不需要進(jìn)行對象的移動,,并且僅對不存活的對象進(jìn)行處理,在存活對象比較多的情況下極為高效,,但由于標(biāo)記-清除算法直接回收不存活的對象,,因此會造成內(nèi)存碎片! 2,、 復(fù)制算法
復(fù)制算法采用從根集合掃描,,并將存活對象復(fù)制到一塊新的,,沒有使用過的空間中,這種算法當(dāng)控件存活的對象比較少時(shí),,極為高效,,但是帶來的成本是需要一塊內(nèi)存交換空間用于進(jìn)行對象的移動。也就是我們前面提到的 s0 s1等空間,。
3,、 標(biāo)記-整理算法
標(biāo)記 -整理算法采用標(biāo)記-清除算法一樣的方式進(jìn)行對象的標(biāo)記,但在清除時(shí)不同,,在回收不存活的對象占用的空間后,,會將所有的存活對象往左端空閑空間移動,并更新對應(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)行垃圾回收,下圖展示了不同生代不通垃圾回收器,,其中兩個(gè)回收器之間有連線表示這兩個(gè)回收器可以同時(shí)使用,。
而這些垃圾回收器又分為串行回收方式、并行回收方式合并發(fā)回收方式執(zhí)行,,分別運(yùn)用于不同的場景,。如下圖所示
下面我們來逐一介紹一下每個(gè)垃圾回收器。
1,、 Serial收集器 看名字我們都可以看的出來,,這個(gè)屬于串行收集器。其運(yùn)行示意圖如下
Serial 收集器是歷史最悠久的一個(gè)回收器,,JDK1.3之前廣泛使用這個(gè)收集器,目前也是ClientVM下 ServerVM 4核4GB以下機(jī)器的默認(rèn)垃圾回收器,。串行收集器并不是只能使用一個(gè)CPU進(jìn)行收集,,而是當(dāng)JVM需要進(jìn)行垃圾回收的時(shí)候,需要中斷所有的用戶線程,,知道它回收結(jié)束為止,,因此又號稱“Stop The World” 的垃圾回收器,。注意,,JVM中文名稱為java虛擬機(jī),因此它就像一臺虛擬的電腦一樣在工作,,而其中的每一個(gè)線程就被認(rèn)為是JVM的一個(gè)處理器,因此大家看到圖中的CPU0,、CPU1實(shí)際為用戶的線程,,而不是真正機(jī)器的CPU,大家不要誤解哦,。
串行回收方式適合低端機(jī)器,,是Client模式下的默認(rèn)收集器,對CPU和內(nèi)存的消耗不高,,適合用戶交互比較少,,后臺任務(wù)較多的系統(tǒng)。 Serial收集器默認(rèn)新舊生代的回收器搭配為Serial+ SerialOld 2,、 ParNew收集器 ParNew收集器其實(shí)就是多線程版本的Serial收集器,,其運(yùn)行示意圖如下
同樣有 Stop The World的問題,他是多CPU模式下的首選回收器(該回收器在單CPU的環(huán)境下回收效率遠(yuǎn)遠(yuǎn)低于Serial收集器,,所以一定要注意場景哦),,也是Server模式下的默認(rèn)收集器。
3,、 ParallelScavenge ParallelScavenge又被稱為是吞吐量優(yōu)先的收集器,,器運(yùn)行示意圖如下
ParallelScavenge 所提到的吞吐量=程序運(yùn)行時(shí)間/(JVM執(zhí)行回收的時(shí)間+程序運(yùn)行時(shí)間),假設(shè)程序運(yùn)行了100分鐘,JVM的垃圾回收占用1分鐘,,那么吞吐量就是99%。在當(dāng)今網(wǎng)絡(luò)告訴發(fā)達(dá)的今天,,良好的響應(yīng)速度是提升用戶體驗(yàn)的一個(gè)重要指標(biāo),,多核并行云計(jì)算的發(fā)展要求程序盡可能的使用CPU和內(nèi)存資源,盡快的計(jì)算出最終結(jié)果,,因此在交互不多的云端,,比較適合使用該回收器。
4,、 ParallelOld ParallelOld是老生代并行收集器的一種,,使用標(biāo)記整理算法,、是老生代吞吐量優(yōu)先的一個(gè)收集器。這個(gè)收集器是JDK1.6之后剛引入的一款收集器,,我們看之前那個(gè)圖之間的關(guān)聯(lián)關(guān)系可以看到,,早期沒有ParallelOld之前,吞吐量優(yōu)先的收集器老生代只能使用串行回收收集器,,大大的拖累了吞吐量優(yōu)先的性能,,自從JDK1.6之后,才能真正做到較高效率的吞吐量優(yōu)先,。其運(yùn)行示意圖如下
5,、 SerialOld
SerialOld是舊生代Client模式下的默認(rèn)收集器,單線程執(zhí)行,;在JDK1.6之前也是ParallelScvenge回收新生代模式下舊生代的默認(rèn)收集器,,同時(shí)也是并發(fā)收集器CMS回收失敗后的備用收集器。其運(yùn)行示意圖如下
6,、 CMS
CMS又稱響應(yīng)時(shí)間優(yōu)先(最短回收停頓)的回收器,,使用并發(fā)模式回收垃圾,使用標(biāo)記-清除算法,,CMS對CPU是非常敏感的,,它的回收線程數(shù)=(CPU+3)/4,因此當(dāng)CPU是2核的實(shí)惠,,回收線程將占用的CPU資源的50%,而當(dāng)CPU核心數(shù)為4時(shí)僅占用25%,。他的運(yùn)行示意圖如下
CMS 模式主要分為4個(gè)過程
在初始標(biāo)記的時(shí)候,,需要中斷所有用戶線程,在并發(fā)標(biāo)記階段,,用戶線程和標(biāo)記線程 并發(fā)執(zhí)行,,而在這個(gè)過程中,隨著內(nèi)存引用關(guān)系的變化,,可能會發(fā)生原來標(biāo)記的對象被釋放,,進(jìn)而引發(fā)新的垃圾,因此可能會產(chǎn)生一系列的浮動垃圾,,不能被回收,。
CMS 為了確保能夠掃描到所有的對象,避免在Initial Marking 中還有未標(biāo)識到的對象,,采用的方法為找到標(biāo)記了的對象,,并將這些對象放入Stack 中,掃描時(shí)尋找此對象依賴的對象,如果依賴的對象的地址在其之前,,則將此對象進(jìn)行標(biāo)記,,并同時(shí)放入Stack 中,如依賴的對象地址在其之后,,則僅標(biāo)記該對象,。 在進(jìn)行Concurrent Marking 時(shí)minor GC 也可能會同時(shí)進(jìn)行,這個(gè)時(shí)候很容易造成舊生代對象引用關(guān)系改變,,CMS 為了應(yīng)對這樣的并發(fā)現(xiàn)象,,提供了一個(gè)Mod Union Table 來進(jìn)行記錄,在這個(gè)Mod Union Table中記錄每次minor GC 后修改了的Card 的信息,。這也是ParallelScavenge不能和CMS一起使用的原因,。 CMS產(chǎn)生浮動垃圾的情況請見如下示意圖
在運(yùn)行回收過后,c就變成了浮動垃圾,。 由于CMS會產(chǎn)生浮動垃圾,,當(dāng)回收過后,浮動垃圾如果產(chǎn)生過多,,同時(shí)因?yàn)槭褂脴?biāo)記-清除算法會產(chǎn)生碎片,,可能會導(dǎo)致回收過后的連續(xù)空間仍然不能容納新生代移動過來或者新創(chuàng)建的大資源,因此會導(dǎo)致CMS回收失敗,,進(jìn)而觸發(fā)另外一次FULL GC,,而這時(shí)候則采用SerialOld進(jìn)行二次回收。 同時(shí)CMS因?yàn)榭赡墚a(chǎn)生浮動垃圾,,而CMS在執(zhí)行回收的同時(shí)新生代也有可能在進(jìn)行回收操作,,為了保證舊生代能夠存放新生代轉(zhuǎn)移過來的數(shù)據(jù),CMS在舊生代內(nèi)存到達(dá)全部容量的68%就觸發(fā)了CMS的回收,! 7,、 GarbageFirst(G1 ) 我們再來看垃圾回收器的總圖,剛才我們可以看到,,我在圖上標(biāo)記了一個(gè),?,其實(shí)這是一個(gè)新的垃圾回收器,,既可以回收新生代也可以回收舊生代,,SunHotSpot 1.6u14以上EarlyAccess版本加入了這個(gè)回收器,sun公司預(yù)期SunHotSpot1.7發(fā)布正式版,,他是商用高性能垃圾回收器,,通過重新劃分內(nèi)存區(qū)域,整合優(yōu)化CMS,,同時(shí)注重吞吐量和響應(yīng)時(shí)間,但是杯具的是被oracle收購之后這個(gè)收集器屬于商用收費(fèi)收集器,,因此目前基本上沒有人使用,,我們在這里也就不多介紹,,更多信息可以參考oracle新版本JDK說明。 下面我們再來看下JVM的一些內(nèi)存分配與回收策略 1,、 優(yōu)先在Edon上分配對象
從運(yùn)行結(jié)果我們可以很清晰的看到,,eden有8MB的存儲控件(通過參數(shù)配置),前6MB的數(shù)據(jù)優(yōu)先分配到eden區(qū)域,當(dāng)下一個(gè)2MB存放時(shí),,因空間已滿,,觸發(fā)一次GC,但是這部分?jǐn)?shù)據(jù)因?yàn)闆]有回收(引用還在,,當(dāng)賦值為null后則不會轉(zhuǎn)移),,數(shù)據(jù)會被復(fù)制到s0區(qū)域,但是s0區(qū)域不夠存儲,,因此直接放入老生代區(qū)域,,新的2MB數(shù)據(jù)存放在eden區(qū)域 2、 大對象直接進(jìn)入老生代
我們看到,,沒有觸發(fā)GC日志,,而數(shù)據(jù)是直接進(jìn)入老生代的 3、 年長者(長期存活對象)進(jìn)入老生代
從代碼中我們可以看到,,當(dāng)testCase1劃分為0.25MB數(shù)據(jù),,進(jìn)行多次大對象創(chuàng)建之后,testCase1應(yīng)該在GC執(zhí)行之后被復(fù)制到s0區(qū)域(s0足以容納testCase1),,但是我們設(shè)置了對象的年齡為1,,即超過1歲便進(jìn)入老生代,因此GC執(zhí)行2次后testCase1直接被復(fù)制到了老生代,,而默認(rèn)進(jìn)入老生代的年齡為15,。我們通過profilter的監(jiān)控工具可以很清楚的看到對象的年齡,如圖所示
右側(cè)的年代數(shù)目就是對象的年齡
4,、 群體效應(yīng)(大批中年對象進(jìn)入老生代)
我們看到,,當(dāng)創(chuàng)建后testCase3,testCase2被移動到s0區(qū)域,,當(dāng)被釋放后,,繼續(xù)創(chuàng)建testCase3,按理說testCase2應(yīng)該移動到s1區(qū)域,,但是因?yàn)槌^了s1區(qū)域的1/2,,因此直接進(jìn)入老生代 5、 擔(dān)保GC(擔(dān)保minorGC) 擔(dān)保GC就是擔(dān)保minorGC能夠滿足當(dāng)前的存儲空間,,而無需觸發(fā)老生代的回收,,由于大部分對象都是朝生夕死的,因此,在實(shí)際開發(fā)中這種很起效,,但是也有可能會發(fā)生擔(dān)保失敗的情況,,當(dāng)擔(dān)保失敗的時(shí)候會觸發(fā)FullGC,但是失敗畢竟是少數(shù),,因此這種一般是很劃算的,。
結(jié)果分析
我們可以很清楚的看到,,當(dāng)無擔(dān)保的時(shí)候,觸發(fā)了一次FullGC 而有擔(dān)保的情況下,,只有monorGC則完成了回收,,大大提升了效率。 當(dāng)我們注釋掉對應(yīng)的代碼
的時(shí)候,,就會引發(fā)擔(dān)保失敗,,如下圖所示
JVM 默認(rèn)情況是是開啟擔(dān)保的,無需設(shè)置參數(shù),。
|
|