淺析JAVA的垃圾回收機(jī)制(GC)

1.什么是垃圾回收可帽?

? ? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供的一種用于在空閑時(shí)間不定時(shí)回收無(wú)任何對(duì)象引用的對(duì)象占據(jù)的內(nèi)存空間的一種機(jī)制。

? ? 注意:垃圾回收回收的是無(wú)任何引用的對(duì)象占據(jù)的內(nèi)存空間而不是對(duì)象本身嘲驾。換言之,垃圾回收只會(huì)負(fù)責(zé)釋放那些對(duì)象占有的內(nèi)存厌衙。對(duì)象是個(gè)抽象的詞距淫,包括引用和其占據(jù)的內(nèi)存空間绞绒。當(dāng)對(duì)象沒(méi)有任何引用時(shí)其占據(jù)的內(nèi)存空間隨即被收回備用婶希,此時(shí)對(duì)象也就被銷毀。但不能說(shuō)是回收對(duì)象蓬衡,可以理解為一種文字游戲喻杈。

分析:

? ? 引用:如果Reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址彤枢,就稱這塊內(nèi)存代表著一個(gè)引用。(引用都有哪些筒饰?對(duì)垃圾回收又有什么影響缴啡?

? ? 垃圾:無(wú)任何對(duì)象引用的對(duì)象(怎么通過(guò)算法找到這些對(duì)象呢?)瓷们。

? ? 回收:清理“垃圾”占用的內(nèi)存空間而非對(duì)象本身(怎么通過(guò)算法實(shí)現(xiàn)回收呢业栅?)。

? ? 發(fā)生地點(diǎn):一般發(fā)生在堆內(nèi)存中谬晕,因?yàn)榇蟛糠值膶?duì)象都儲(chǔ)存在堆內(nèi)存中(堆內(nèi)存為了配合垃圾回收有什么不同區(qū)域劃分碘裕,各區(qū)域有什么不同?)攒钳。

? ? 發(fā)生時(shí)間:程序空閑時(shí)間不定時(shí)回收(回收的執(zhí)行機(jī)制是什么帮孔?是否可以通過(guò)顯示調(diào)用函數(shù)的方式來(lái)確定的進(jìn)行回收過(guò)程?

? ? 帶著這些問(wèn)題我們開(kāi)始進(jìn)一步的分析不撑。

2.Java中的對(duì)象引用

? (1)強(qiáng)引用(Strong Reference):如“Object obj = new Object()”文兢,這類引用是Java程序中最普遍的。只要強(qiáng)引用還存在焕檬,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象姆坚。

? (2)軟引用(Soft Reference):它用來(lái)描述一些可能還有用,但并非必須的對(duì)象实愚。在系統(tǒng)內(nèi)存不夠用時(shí)旷偿,這類引用關(guān)聯(lián)的對(duì)象將被垃圾收集器回收。JDK1.2之后提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用爆侣。

? (3)弱引用(Weak Reference):它也是用來(lái)描述非須對(duì)象的萍程,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前兔仰。當(dāng)垃圾收集器工作時(shí)茫负,無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象乎赴。在JDK1.2之后忍法,提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。

? (4)虛引用(Phantom Reference):最弱的一種引用關(guān)系榕吼,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響饿序,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知羹蚣。JDK1.2之后提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用原探。

? ? 區(qū)分Java對(duì)象和對(duì)象引用請(qǐng)參考區(qū)分JAVA中的對(duì)象和引用

3.判斷對(duì)象是否是垃圾的算法。

? ? ? Java語(yǔ)言規(guī)范沒(méi)有明確地說(shuō)明JVM使用哪種垃圾回收算法咽弦,但是任何一種垃圾回收算法一般要做2件基本的事情:(1)找到所有存活對(duì)象徒蟆;(2)回收被無(wú)用對(duì)象占用的內(nèi)存空間,使該空間可被程序再次使用型型。

3.1引用計(jì)數(shù)算法(Reference Counting Collector)

? ? 堆中每個(gè)對(duì)象(不是引用)都有一個(gè)引用計(jì)數(shù)器段审。當(dāng)一個(gè)對(duì)象被創(chuàng)建并初始化賦值后,該變量計(jì)數(shù)設(shè)置為1闹蒜。每當(dāng)有一個(gè)地方引用它時(shí)寺枉,計(jì)數(shù)器值就加1(a = b, b被引用绷落,則b引用的對(duì)象計(jì)數(shù)+1)型凳。當(dāng)引用失效時(shí)(一個(gè)對(duì)象的某個(gè)引用超過(guò)了生命周期(出作用域后)或者被設(shè)置為一個(gè)新值時(shí)),計(jì)數(shù)器值就減1嘱函。任何引用計(jì)數(shù)為0的對(duì)象可以被當(dāng)作垃圾收集甘畅。當(dāng)一個(gè)對(duì)象被垃圾收集時(shí),它引用的任何對(duì)象計(jì)數(shù)減1往弓。

? ? 優(yōu)點(diǎn):引用計(jì)數(shù)收集器執(zhí)行簡(jiǎn)單疏唾,判定效率高,交織在程序運(yùn)行中函似。對(duì)程序不被長(zhǎng)時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利(OC的內(nèi)存管理使用該算法)槐脏。

? ? 缺點(diǎn): 難以檢測(cè)出對(duì)象之間的循環(huán)引用。同時(shí)撇寞,引用計(jì)數(shù)器增加了程序執(zhí)行的開(kāi)銷顿天。所以Java語(yǔ)言并沒(méi)有選擇這種算法進(jìn)行垃圾回收。

? ? 早期的JVM使用引用計(jì)數(shù)蔑担,現(xiàn)在大多數(shù)JVM采用對(duì)象引用遍歷(根搜索算法)牌废。

3.2根搜索算法(Tracing Collector)

首先了解一個(gè)概念:根集(Root Set)

? ? 所謂根集(Root Set)就是正在執(zhí)行的Java程序可以訪問(wèn)的引用變量(注意:不是對(duì)象)的集合(包括局部變量、參數(shù)啤握、類變量)鸟缕,程序可以使用引用變量訪問(wèn)對(duì)象的屬性和調(diào)用對(duì)象的方法。

? ? 這種算法的基本思路:

?(1)通過(guò)一系列名為“GC Roots”的對(duì)象作為起始點(diǎn)排抬,尋找對(duì)應(yīng)的引用節(jié)點(diǎn)懂从。

(2)找到這些引用節(jié)點(diǎn)后,從這些節(jié)點(diǎn)開(kāi)始向下繼續(xù)尋找它們的引用節(jié)點(diǎn)蹲蒲。

?(3)重復(fù)(2)番甩。

?(4)搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)届搁,就證明此對(duì)象是不可用的缘薛。

? ? Java和C#中都是采用根搜索算法來(lái)判定對(duì)象是否存活的窍育。

標(biāo)記可達(dá)對(duì)象:

? ? JVM中用到的所有現(xiàn)代GC算法在回收前都會(huì)先找出所有仍存活的對(duì)象。根搜索算法是從離散數(shù)學(xué)中的圖論引入的掩宜,程序把所有的引用關(guān)系看作一張圖。下圖3.0中所展示的JVM中的內(nèi)存布局可以用來(lái)很好地闡釋這一概念:

圖 3.0 標(biāo)記(marking)對(duì)象

? 首先么翰,垃圾回收器將某些特殊的對(duì)象定義為GC根對(duì)象牺汤。所謂的GC根對(duì)象包括:

(1)虛擬機(jī)棧中引用的對(duì)象(棧幀中的本地變量表);

(2)方法區(qū)中的常量引用的對(duì)象浩嫌;

(3)方法區(qū)中的類靜態(tài)屬性引用的對(duì)象檐迟;

(4)本地方法棧中JNI(Native方法)的引用對(duì)象。

(5)活躍線程码耐。

? ? 接下來(lái)追迟,垃圾回收器會(huì)對(duì)內(nèi)存中的整個(gè)對(duì)象圖進(jìn)行遍歷,它先從GC根對(duì)象開(kāi)始骚腥,然后是根對(duì)象引用的其它對(duì)象敦间,比如實(shí)例變量∈回收器將訪問(wèn)到的所有對(duì)象都標(biāo)記為存活廓块。

? ? 存活對(duì)象在上圖中被標(biāo)記為藍(lán)色。當(dāng)標(biāo)記階段完成了之后契沫,所有的存活對(duì)象都已經(jīng)被標(biāo)記完了带猴。其它的那些(上圖中灰色的那些)也就是GC根對(duì)象不可達(dá)的對(duì)象,也就是說(shuō)你的應(yīng)用不會(huì)再用到它們了懈万。這些就是垃圾對(duì)象拴清,回收器將會(huì)在接下來(lái)的階段中清除它們。

關(guān)于標(biāo)記階段有幾個(gè)關(guān)鍵點(diǎn)是值得注意的:

? ? (1)開(kāi)始進(jìn)行標(biāo)記前会通,需要先暫停應(yīng)用線程口予,否則如果對(duì)象圖一直在變化的話是無(wú)法真正去遍歷它的。暫停應(yīng)用線程以便JVM可以盡情地收拾家務(wù)的這種情況又被稱之為安全點(diǎn)(Safe Point)涕侈,這會(huì)觸發(fā)一次Stop The World(STW)暫停苹威。觸發(fā)安全點(diǎn)的原因有許多,但最常見(jiàn)的應(yīng)該就是垃圾回收了驾凶。

? ? (2)暫停時(shí)間的長(zhǎng)短并不取決于堆內(nèi)對(duì)象的多少也不是堆的大小牙甫,而是存活對(duì)象的多少。因此调违,調(diào)高堆的大小并不會(huì)影響到標(biāo)記階段的時(shí)間長(zhǎng)短窟哺。

? ? (3)在根搜索算法中,要真正宣告一個(gè)對(duì)象死亡技肩,至少要經(jīng)歷兩次標(biāo)記過(guò)程:

? ? ? 1.如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒(méi)有與GC Roots相連接的引用鏈且轨,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選浮声。篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize()方法(可看作析構(gòu)函數(shù),類似于OC中的dealloc旋奢,Swift中的deinit)泳挥。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò)至朗,虛擬機(jī)將這兩種情況都視為沒(méi)有必要執(zhí)行屉符。

? ? ? 2.如果該對(duì)象被判定為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為F-Queue隊(duì)列中锹引,并在稍后由一條由虛擬機(jī)自動(dòng)建立的矗钟、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行finalize()方法。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次)嫌变,稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記吨艇,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對(duì)象重新引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可腾啥。而如果對(duì)象這時(shí)還沒(méi)有關(guān)聯(lián)到任何鏈上的引用东涡,那它就會(huì)被回收掉。

? ? ?(4)實(shí)際上GC判斷對(duì)象是否可達(dá)看的是強(qiáng)引用倘待。

? ? 當(dāng)標(biāo)記階段完成后软啼,GC開(kāi)始進(jìn)入下一階段,刪除不可達(dá)對(duì)象延柠。

4.回收垃圾對(duì)象內(nèi)存的算法

4.1 Tracing算法(Tracing Collector) 或 標(biāo)記—清除算法

? ? 標(biāo)記—清除算法是最基礎(chǔ)的收集算法祸挪,為了解決引用計(jì)數(shù)法的問(wèn)題而提出。它使用了根集的概念贞间,它分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象贿条,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,它的標(biāo)記過(guò)程其實(shí)就是前面的根搜索算法中判定垃圾對(duì)象的標(biāo)記過(guò)程增热。

? ? 優(yōu)點(diǎn):不需要進(jìn)行對(duì)象的移動(dòng)整以,并且僅對(duì)不存活的對(duì)象進(jìn)行處理,在存活對(duì)象比較多的情況下極為高效峻仇。

? ? ? 缺點(diǎn):(1)標(biāo)記和清除過(guò)程的效率都不高公黑。(這種方法需要使用一個(gè)空閑列表來(lái)記錄所有的空閑區(qū)域以及大小。對(duì)空閑列表的管理會(huì)增加分配對(duì)象時(shí)的工作量摄咆。如圖4.1所示凡蚜。)。(2)標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片吭从。雖然空閑區(qū)域的大小是足夠的朝蜘,但卻可能沒(méi)有一個(gè)單一區(qū)域能夠滿足這次分配所需的大小,因此本次分配還是會(huì)失斏稹(在Java中就是一次OutOfMemoryError)不得不觸發(fā)另一次垃圾收集動(dòng)作谱醇。如圖4.2所示暇仲。

算法示意圖:

圖 4.0 ?標(biāo)記—清除算法


圖4.1 標(biāo)記—清除算法

4.2 Compacting算法(Compacting Collector) 或 標(biāo)記—整理算法

? ? ? 該算法標(biāo)記的過(guò)程與標(biāo)記—清除算法中的標(biāo)記過(guò)程一樣,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同副渴,它不是直接對(duì)可回收對(duì)象進(jìn)行清理奈附,而是讓所有的對(duì)象都向一端移動(dòng)振峻,然后直接清理掉端邊界以外的內(nèi)存售貌。在基于Compacting算法的收集器的實(shí)現(xiàn)中容为,一般增加句柄和句柄表州弟。

? ? ? 優(yōu)點(diǎn):(1)經(jīng)過(guò)整理之后,新對(duì)象的分配只需要通過(guò)指針碰撞便能完成(Pointer Bumping)哼鬓,相當(dāng)簡(jiǎn)單。(2)使用這種方法空閑區(qū)域的位置是始終可知的,也不會(huì)再有碎片的問(wèn)題了菇篡。

? ? ? 缺點(diǎn):GC暫停的時(shí)間會(huì)增長(zhǎng),因?yàn)槟阈枰獙⑺械膶?duì)象都拷貝到一個(gè)新的地方一喘,還得更新它們的引用地址驱还。

算法示意圖:

圖4.2 標(biāo)記—整理算法


圖4.3 標(biāo)記—整理算法

4.3 Copying算法(Copying Collector)

? ? ? 該算法的提出是為了克服句柄的開(kāi)銷和解決堆碎片的垃圾回收。它將內(nèi)存按容量分為大小相等的兩塊凸克,每次只使用其中的一塊(對(duì)象面)议蟆,當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊內(nèi)存上面(空閑面)萎战,然后再把已使用過(guò)的內(nèi)存空間一次清理掉咐容。

? ? ? 復(fù)制算法比較適合于新生代(短生存期的對(duì)象),在老年代(長(zhǎng)生存期的對(duì)象)中蚂维,對(duì)象存活率比較高戳粒,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低虫啥,所以老年代一般會(huì)選用其他算法蔚约,如標(biāo)記—整理算法。一種典型的基于Coping算法的垃圾回收是stop-and-copy算法涂籽,它將堆分成對(duì)象區(qū)和空閑區(qū)苹祟,在對(duì)象區(qū)與空閑區(qū)的切換過(guò)程中,程序暫停執(zhí)行评雌。

? ? ? 優(yōu)點(diǎn):(1)標(biāo)記階段和復(fù)制階段可以同時(shí)進(jìn)行树枫。(2)每次只對(duì)一塊內(nèi)存進(jìn)行回收,運(yùn)行高效景东。(3)只需移動(dòng)棧頂指針团赏,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單耐薯。(4)內(nèi)存回收時(shí)不用考慮內(nèi)存碎片的出現(xiàn)(得活動(dòng)對(duì)象所占的內(nèi)存空間之間沒(méi)有空閑間隔)舔清。

? ? ? 缺點(diǎn):需要一塊能容納下所有存活對(duì)象的額外的內(nèi)存空間丝里。因此,可一次性分配的最大內(nèi)存縮小了一半体谒。

算法示意圖:

圖4.4 Copying算法


圖4.4 Copying算法

4.4 ?Adaptive算法(Adaptive Collector)

? ? ? 在特定的情況下杯聚,一些垃圾收集算法會(huì)優(yōu)于其它算法∈阊鳎基于Adaptive算法的垃圾收集器就是監(jiān)控當(dāng)前堆的使用情況幌绍,并將選擇適當(dāng)算法的垃圾收集器。

5 ?Java的堆內(nèi)存(Java Heap Memory)

? ? ? Java的堆內(nèi)存基于Generation算法(Generational Collector)劃分為新生代故响、年老代和持久代傀广。新生代又被進(jìn)一步劃分為Eden和Survivor區(qū),最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)組成彩届。所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在堆中分配伪冰,其大小可以通過(guò)-Xmx和-Xms來(lái)控制。

? ? ? 分代收集樟蠕,是基于這樣一個(gè)事實(shí):不同的對(duì)象的生命周期是不一樣的贮聂。因此,可以將不同生命周期的對(duì)象分代寨辩,不同的代采取不同的回收算法(4.1-4.3)進(jìn)行垃圾回收(GC)吓懈,以便提高回收效率。

堆內(nèi)存分區(qū)示意圖:

圖5.0 Java Heap Memory


圖5.1 ?Java Heap Memory

Java的內(nèi)存空間除了堆內(nèi)存還有其他部分:

1)棧

? ? 每個(gè)線程執(zhí)行每個(gè)方法的時(shí)候都會(huì)在棧中申請(qǐng)一個(gè)棧幀靡狞,每個(gè)棧幀包括局部變量區(qū)和操作數(shù)棧耻警,用于存放此次方法調(diào)用過(guò)程中的臨時(shí)變量、參數(shù)和中間結(jié)果甸怕。

2)本地方法棧

? ? 用于支持native方法的執(zhí)行甘穿,存儲(chǔ)了每個(gè)native方法調(diào)用的狀態(tài)。

4)方法區(qū)

? ? 存放了要加載的類信息蕾各、靜態(tài)變量扒磁、final類型的常量、屬性和方法信息式曲。JVM用持久代(PermanetGeneration)來(lái)存放方法區(qū)妨托,可通過(guò)-XX:PermSize和-XX:MaxPermSize來(lái)指定最小值和最大值。

詳細(xì)可以參考:Java內(nèi)存區(qū)域和內(nèi)存溢出吝羞。

5.1堆內(nèi)存分配區(qū)域:

1.年輕代(Young Generation)

? ? ? 幾乎所有新生成的對(duì)象首先都是放在年輕代的兰伤。新生代內(nèi)存按照8:1:1的比例分為一個(gè)Eden區(qū)和兩個(gè)Survivor(Survivor0,Survivor1)區(qū)。大部分對(duì)象在Eden區(qū)中生成钧排。當(dāng)新對(duì)象生成敦腔,Eden Space申請(qǐng)失敗(因?yàn)榭臻g不足等)恨溜,則會(huì)發(fā)起一次GC(Scavenge GC)符衔≌仪埃回收時(shí)先將Eden區(qū)存活對(duì)象復(fù)制到一個(gè)Survivor0區(qū),然后清空Eden區(qū)判族,當(dāng)這個(gè)Survivor0區(qū)也存放滿了時(shí)躺盛,則將Eden區(qū)和Survivor0區(qū)存活對(duì)象復(fù)制到另一個(gè)Survivor1區(qū),然后清空Eden和這個(gè)Survivor0區(qū)形帮,此時(shí)Survivor0區(qū)是空的槽惫,然后將Survivor0區(qū)和Survivor1區(qū)交換,即保持Survivor1區(qū)為空辩撑, 如此往復(fù)界斜。當(dāng)Survivor1區(qū)不足以存放 Eden和Survivor0的存活對(duì)象時(shí),就將存活對(duì)象直接存放到老年代合冀。當(dāng)對(duì)象在Survivor區(qū)躲過(guò)一次GC的話各薇,其對(duì)象年齡便會(huì)加1,默認(rèn)情況下水慨,如果對(duì)象年齡達(dá)到15歲得糜,就會(huì)移動(dòng)到老年代中敬扛。若是老年代也滿了就會(huì)觸發(fā)一次Full GC晰洒,也就是新生代、老年代都進(jìn)行回收啥箭。新生代大小可以由-Xmn來(lái)控制谍珊,也可以用-XX:SurvivorRatio來(lái)控制Eden和Survivor的比例。

2.年老代(Old Generation)

? ? ? 在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象急侥,就會(huì)被放到年老代中砌滞。因此,可以認(rèn)為年老代中存放的都是一些生命周期較長(zhǎng)的對(duì)象坏怪。內(nèi)存比新生代也大很多(大概比例是1:2)贝润,當(dāng)老年代內(nèi)存滿時(shí)觸發(fā)Major GC即Full GC,F(xiàn)ull GC發(fā)生頻率比較低铝宵,老年代對(duì)象存活時(shí)間比較長(zhǎng)打掘,存活率標(biāo)記高。一般來(lái)說(shuō)鹏秋,大對(duì)象會(huì)被直接分配到老年代尊蚁。所謂的大對(duì)象是指需要大量連續(xù)存儲(chǔ)空間的對(duì)象,最常見(jiàn)的一種大對(duì)象就是大數(shù)組侣夷。比如:

? ? ? byte[] data = new byte[4*1024*1024]

? ? ? 這種一般會(huì)直接在老年代分配存儲(chǔ)空間横朋。

? ? ? 當(dāng)然分配的規(guī)則并不是百分之百固定的,這要取決于當(dāng)前使用的是哪種垃圾收集器組合和JVM的相關(guān)參數(shù)百拓。

3.持久代(Permanent Generation)

? ? ? 用于存放靜態(tài)文件(class類琴锭、方法)和常量等晰甚。持久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class决帖,例如Hibernate 等压汪,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類。對(duì)永久代的回收主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類古瓤。

? ? ? 永久代空間在Java SE8特性中已經(jīng)被移除止剖。取而代之的是元空間(MetaSpace)。因此不會(huì)再出現(xiàn)“java.lang.OutOfMemoryError: PermGen error”錯(cuò)誤落君。

5.2 堆內(nèi)存分配策略明確以下三點(diǎn):

(1)對(duì)象優(yōu)先在Eden分配穿香。

(2)大對(duì)象直接進(jìn)入老年代。

(3)長(zhǎng)期存活的對(duì)象將進(jìn)入老年代绎速。

5.3 對(duì)垃圾回收機(jī)制說(shuō)明以下三點(diǎn):

? ? ? 新生代GC(Minor GC/Scavenge GC):發(fā)生在新生代的垃圾收集動(dòng)作皮获。因?yàn)镴ava對(duì)象大多都具有朝生夕滅的特性,因此Minor GC非常頻繁(不一定等Eden區(qū)滿了才觸發(fā))纹冤,一般回收速度也比較快洒宝。在新生代中,每次垃圾收集時(shí)都會(huì)發(fā)現(xiàn)有大量對(duì)象死去萌京,只有少量存活雁歌,因此可選用復(fù)制算法來(lái)完成收集。

? ? 老年代GC(Major GC/Full GC):發(fā)生在老年代的垃圾回收動(dòng)作知残。Major GC靠瞎,經(jīng)常會(huì)伴隨至少一次Minor GC。由于老年代中的對(duì)象生命周期比較長(zhǎng)求妹,因此Major GC并不頻繁乏盐,一般都是等待老年代滿了后才進(jìn)行Full GC,而且其速度一般會(huì)比Minor GC慢10倍以上制恍。另外父能,如果分配了Direct Memory,在老年代中進(jìn)行Full GC時(shí)净神,會(huì)順便清理掉Direct Memory中的廢棄對(duì)象何吝。而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保强挫,就必須使用標(biāo)記—清除算法或標(biāo)記—整理算法來(lái)進(jìn)行回收岔霸。

? ? ? 新生代采用空閑指針的方式來(lái)控制GC觸發(fā),指針保持最后一個(gè)分配的對(duì)象在新生代區(qū)間的位置俯渤,當(dāng)有新的對(duì)象要分配內(nèi)存時(shí)呆细,用于檢查空間是否足夠,不夠就觸發(fā)GC。當(dāng)連續(xù)分配對(duì)象時(shí)絮爷,對(duì)象會(huì)逐漸從Eden到Survivor趴酣,最后到老年代。

? ? ? 用Java VisualVM來(lái)查看坑夯,能明顯觀察到新生代滿了后岖寞,會(huì)把對(duì)象轉(zhuǎn)移到舊生代,然后清空繼續(xù)裝載柜蜈,當(dāng)老年代也滿了后仗谆,就會(huì)報(bào)outofmemory的異常,如下圖所示:

圖5.2 垃圾回收分析

如何使用Java VisualVM 進(jìn)行垃圾回收的監(jiān)視和分析請(qǐng)參考:垃圾回收的監(jiān)視和分析淑履。

6 ?垃圾回收器(GC)

6.1 按執(zhí)行機(jī)制劃分Java有四種類型的垃圾回收器:

(1)串行垃圾回收器(Serial Garbage Collector)

(2)并行垃圾回收器(Parallel Garbage Collector)

(3)并發(fā)標(biāo)記掃描垃圾回收器(CMS Garbage Collector)

(4)G1垃圾回收器(G1 Garbage Collector)

圖6.0 GC

? ? ?每種類型都有自己的優(yōu)勢(shì)與劣勢(shì)隶垮,在很大程度上有 所不同并且可以為我們提供完全不同的應(yīng)用程序性能。重要的是秘噪,我們編程的時(shí)候可以通過(guò)向JVM傳遞參數(shù)選擇垃圾回收器類型狸吞。每種類型理解每種類型的垃圾回收器并且根據(jù)應(yīng)用程序選擇進(jìn)行正確的選擇是非常重要的。

1指煎、串行垃圾回收器

? ? ? 串行垃圾回收器通過(guò)持有應(yīng)用程序所有的線程進(jìn)行工作蹋偏。它為單線程環(huán)境設(shè)計(jì),只使用一個(gè)單獨(dú)的線程進(jìn)行垃圾回收至壤,通過(guò)凍結(jié)所有應(yīng)用程序線程進(jìn)行工作威始,所以可能不適合服務(wù)器環(huán)境。它最適合的是簡(jiǎn)單的命令行程序(單CPU崇渗、新生代空間較小及對(duì)暫停時(shí)間要求不是非常高的應(yīng)用)字逗。是client級(jí)別默認(rèn)的GC方式京郑。

通過(guò)JVM參數(shù)-XX:+UseSerialGC可以使用串行垃圾回收器宅广。

2、并行垃圾回收器

? ? ? 并行垃圾回收器也叫做 throughput collector 些举。它是JVM的默認(rèn)垃圾回收器跟狱。與串行垃圾回收器不同,它使用多線程進(jìn)行垃圾回收户魏。相似的是驶臊,當(dāng)執(zhí)行垃圾回收的時(shí)候它也會(huì)凍結(jié)所有的應(yīng)用程序線程。

? ? ? 適用于多CPU叼丑、對(duì)暫停時(shí)間要求較短的應(yīng)用上关翎,是server級(jí)別默認(rèn)采用的GC方式○牛可用-XX:+UseParallelGC來(lái)強(qiáng)制指定纵寝,用-XX:ParallelGCThreads=4來(lái)指定線程數(shù)。

3星立、并發(fā)標(biāo)記掃描垃圾回收器

? ? ? 并發(fā)標(biāo)記垃圾回收使用多線程掃描堆內(nèi)存爽茴,標(biāo)記需要清理的實(shí)例并且清理被標(biāo)記過(guò)的實(shí)例葬凳。并發(fā)標(biāo)記垃圾回收器只會(huì)在下面兩種情況持有應(yīng)用程序所有線程。

(1)當(dāng)標(biāo)記的引用對(duì)象在Tenured區(qū)域室奏;

(2)在進(jìn)行垃圾回收的時(shí)候火焰,堆內(nèi)存的數(shù)據(jù)被并發(fā)的改變。

? ? ? 相比并行垃圾回收器胧沫,并發(fā)標(biāo)記掃描垃圾回收器使用更多的CPU來(lái)確保程序的吞吐量昌简。如果我們可以為了更好的程序性能分配更多的CPU,那么并發(fā)標(biāo)記上掃描垃圾回收器是更好的選擇相比并發(fā)垃圾回收器绒怨。

通過(guò)JVM參數(shù) XX:+USeParNewGC 打開(kāi)并發(fā)標(biāo)記掃描垃圾回收器江场。

以上各種GC機(jī)制是需要組合使用的,指定方式由下表所示:

表6.0 ?不同垃圾回收器的組合方式

6.2 垃圾回收的JVM配置

運(yùn)行的垃圾回收器類型:

表6.1 ?GC類型

GC的優(yōu)化配置:

表6.2 ?GC優(yōu)化配置

使用JVM GC 參數(shù)的例子:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar

6.3 HotSpot(JDK 7)虛擬機(jī)提供的幾種垃圾收集器

? ? ? 垃圾收集算法是內(nèi)存回收的理論基礎(chǔ)窖逗,而垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)址否。用戶可以根據(jù)自己的需求組合出各個(gè)年代使用的收集器。

1.Serial(SerialMSC)(Copying算法)

? ? ? Serial收集器是最基本最古老的收集器碎紊,它是一個(gè)單線程收集器佑附,并且在它進(jìn)行垃圾收集時(shí),必須暫停所有用戶線程仗考。Serial收集器是針對(duì)新生代的收集器音同,采用的是Copying算法。

2.Serial Old (標(biāo)記—整理算法)

? ? ? Serial Old收集器是針對(duì)老年代的收集器秃嗜,采用的是Mark-Compact算法权均。它的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單高效,但是缺點(diǎn)是會(huì)給用戶帶來(lái)停頓锅锨。

2.ParNew (Copying算法)

? ? ? ParNew收集器是新生代收集器叽赊,Serial收集器的多線程版本。使用多個(gè)線程進(jìn)行垃圾收集必搞,在多核CPU環(huán)境下有著比Serial更好的表現(xiàn)必指。

3.Parallel Scavenge (Copying算法)

? ? ? Parallel Scavenge收集器是一個(gè)新生代的多線程收集器(并行收集器),它在回收期間不需要暫停其他用戶線程恕洲,其采用的是Copying算法塔橡,該收集器與前兩個(gè)收集器有所不同,它主要是為了達(dá)到一個(gè)可控的吞吐量霜第。追求高吞吐量葛家,高效利用CPU。吞吐量一般為99%泌类。 吞吐量= 用戶線程時(shí)間/(用戶線程時(shí)間+GC線程時(shí)間)癞谒。適合后臺(tái)應(yīng)用等對(duì)交互相應(yīng)要求不高的場(chǎng)景。

4.Parallel Old(ParallelMSC)(標(biāo)記—整理算法)

? ? ? Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多線程和Mark-Compact算法扯俱。吞吐量?jī)?yōu)先书蚪。

5.CMS ?(標(biāo)記—整理算法)

? ? ? CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,它是一種并發(fā)收集器迅栅,采用的是Mark-Sweep算法殊校。高并發(fā)、低停頓读存,追求最短GC回收停頓時(shí)間为流,CPU占用比較高。響應(yīng)時(shí)間快让簿,停頓時(shí)間短敬察,多核CPU 追求高響應(yīng)時(shí)間的選擇。

6.G1

? ? ? G1收集器是當(dāng)今收集器技術(shù)發(fā)展最前沿的成果尔当,它是一款面向服務(wù)端應(yīng)用的收集器莲祸,它能充分利用多CPU、多核環(huán)境椭迎。因此它是一款并行與并發(fā)收集器锐帜,并且它能建立可預(yù)測(cè)的停頓時(shí)間模型。

? ? ? G1垃圾回收器適用于堆內(nèi)存很大的情況畜号,他將堆內(nèi)存分割成不同的區(qū)域缴阎,并且并發(fā)的對(duì)其進(jìn)行垃圾回收。G1也可以在回收內(nèi)存之后對(duì)剩余的堆內(nèi)存空間進(jìn)行壓縮简软。并發(fā)掃描標(biāo)記垃圾回收器在STW情況下壓縮內(nèi)存蛮拔。G1垃圾回收會(huì)優(yōu)先選擇第一塊垃圾最多的區(qū)域。

通過(guò)JVM參數(shù) –XX:+UseG1GC 使用G1垃圾回收器痹升。

Java 8 的新特性:

? ? ? 在使用G1垃圾回收器的時(shí)候建炫,通過(guò) JVM參數(shù) -XX:+UseStringDeduplication 。 我們可以通過(guò)刪除重復(fù)的字符串视卢,只保留一個(gè)char[]來(lái)優(yōu)化堆內(nèi)存踱卵。這個(gè)選擇在Java 8 u 20被引入。

? ? ? 我們給出了全部的幾種Java垃圾回收器据过,需要根據(jù)應(yīng)用場(chǎng)景,硬件性能和吞吐量需求來(lái)決定使用哪一種妒挎。

? ? ? 新生代收集器使用的收集器:Serial绳锅、PraNew、Parallel Scavenge酝掩。

? ? ? 老年代收集器使用的收集器:Serial Old鳞芙、Parallel Old、CMS。

表 6.1 HotSpot 1.6 JVM 垃圾回收器

7 ?垃圾回收?qǐng)?zhí)行時(shí)間和注意事項(xiàng)

? ? GC分為Scavenge GC和Full GC原朝。

? ? Scavenge GC :發(fā)生在Eden區(qū)的垃圾回收驯嘱。

? ? Full GC :對(duì)整個(gè)堆進(jìn)行整理,包括Young喳坠、Tenured和Perm鞠评。Full GC因?yàn)樾枰獙?duì)整個(gè)堆進(jìn)行回收,所以比Scavenge GC要慢壕鹉,因此應(yīng)該盡可能減少Full GC的次數(shù)剃幌。在對(duì)JVM調(diào)優(yōu)的過(guò)程中,很大一部分工作就是對(duì)于FullGC的調(diào)節(jié)晾浴。

? ? 有如下原因可能導(dǎo)致Full GC:

? ? 1.年老代(Tenured)被寫(xiě)滿;

? ? 2.持久代(Perm)被寫(xiě)滿;

? ? 3.System.gc()被顯示調(diào)用;

? ? 4.上一次GC之后Heap的各域分配策略動(dòng)態(tài)變化.

7.1 ?與垃圾回收時(shí)間有關(guān)的兩個(gè)函數(shù)

1. ?System.gc()方法

? ? 命令行參數(shù)監(jiān)視垃圾收集器的運(yùn)行:

? ? 使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法负乡,都可以請(qǐng)求Java的垃圾回收。在命令行中有一個(gè)參數(shù)-verbosegc可以查看Java使用的堆內(nèi)存的情況脊凰,它的格式如下:

? ? java -verbosegc classfile

? ? 需要注意的是抖棘,調(diào)用System.gc()也僅僅是一個(gè)請(qǐng)求(建議)。JVM接受這個(gè)消息后狸涌,并不是立即做垃圾回收钉答,而只是對(duì)幾個(gè)垃圾回收算法做了加權(quán),使垃圾回收操作容易發(fā)生杈抢,或提早發(fā)生数尿,或回收較多而已。

2. ?finalize()方法

? ? 概述:在JVM垃圾回收器收集一個(gè)對(duì)象之前惶楼,一般要求程序調(diào)用適當(dāng)?shù)姆椒ㄡ尫刨Y源右蹦。但在沒(méi)有明確釋放資源的情況下,Java提供了缺省機(jī)制來(lái)終止該對(duì)象以釋放資源歼捐,這個(gè)方法就是finalize()何陆。它的原型為:

protected void finalize() throws Throwable

在finalize()方法返回之后,對(duì)象消失豹储,垃圾收集開(kāi)始執(zhí)行贷盲。原型中的throws Throwable表示它可以拋出任何類型的異常。

? ? 意義:之所以要使用finalize()剥扣,是存在著垃圾回收器不能處理的特殊情況巩剖。假定你的對(duì)象(并非使用new方法)獲得了一塊“特殊”的內(nèi)存區(qū)域,由于垃圾回收器只知道那些顯示地經(jīng)由new分配的內(nèi)存空間钠怯,所以它不知道該如何釋放這塊“特殊”的內(nèi)存區(qū)域佳魔,那么這個(gè)時(shí)候Java允許在類中定義一個(gè)finalize()方法。

? ? 特殊的區(qū)域例如:1)由于在分配內(nèi)存的時(shí)候可能采用了類似 C語(yǔ)言的做法晦炊,而非JAVA的通常new做法鞠鲜。這種情況主要發(fā)生在native method中宁脊,比如native method調(diào)用了C/C++方法malloc()函數(shù)系列來(lái)分配存儲(chǔ)空間,但是除非調(diào)用free()函數(shù)贤姆,否則這些內(nèi)存空間將不會(huì)得到釋放榆苞,那么這個(gè)時(shí)候就可能造成內(nèi)存泄漏。但是由于free()方法是在C/C++中的函數(shù)霞捡,所以finalize()中可以用本地方法來(lái)調(diào)用它坐漏。以釋放這些“特殊”的內(nèi)存空間。2)又或者打開(kāi)的文件資源弄砍,這些資源不屬于垃圾回收器的回收范圍仙畦。

? ? 換言之,finalize()的主要用途是釋放一些其他做法開(kāi)辟的內(nèi)存空間音婶,以及做一些清理工作慨畸。因?yàn)樵贘ava中并沒(méi)有提夠像“析構(gòu)”函數(shù)或者類似概念的函數(shù),要做一些類似清理工作的時(shí)候衣式,必須自己動(dòng)手創(chuàng)建一個(gè)執(zhí)行清理工作的普通方法寸士,也就是override Object這個(gè)類中的finalize()方法。比如:銷毀通知碴卧。

? ? 一旦垃圾回收器準(zhǔn)備好釋放對(duì)象占用的存儲(chǔ)空間弱卡,首先會(huì)去調(diào)用finalize()方法進(jìn)行一些必要的清理工作。只有到下一次再進(jìn)行垃圾回收動(dòng)作的時(shí)候住册,才會(huì)真正釋放這個(gè)對(duì)象所占用的內(nèi)存空間婶博。

? ? JAVA里的對(duì)象并非總會(huì)被垃圾回收器回收。1 對(duì)象可能不被垃圾回收荧飞,2 垃圾回收并不等于“析構(gòu)”凡人,3 垃圾回收只與內(nèi)存有關(guān)。也就是說(shuō)叹阔,并不是如果一個(gè)對(duì)象不再被使用挠轴,是不是要在finalize()中釋放這個(gè)對(duì)象中含有的其它對(duì)象呢?不是的耳幢。因?yàn)闊o(wú)論對(duì)象是如何創(chuàng)建的岸晦,垃圾回收器都會(huì)負(fù)責(zé)釋放那些對(duì)象占有的內(nèi)存。

? ? 當(dāng) finalize() 方法被調(diào)用時(shí)睛藻,JVM 會(huì)釋放該線程上的所有同步鎖启上。

7.2 ?觸發(fā)主GC的條件

? ? 1)當(dāng)應(yīng)用程序空閑時(shí),即沒(méi)有應(yīng)用線程在運(yùn)行時(shí),GC會(huì)被調(diào)用。因?yàn)镚C在優(yōu)先級(jí)最低的線程中進(jìn)行,所以當(dāng)應(yīng)用忙時(shí),GC線程就不會(huì)被調(diào)用,但以下條件除外修档。

? ? 2)Java堆內(nèi)存不足時(shí),GC會(huì)被調(diào)用碧绞。當(dāng)應(yīng)用線程在運(yùn)行,并在運(yùn)行過(guò)程中創(chuàng)建新對(duì)象,若這時(shí)內(nèi)存空間不足,JVM就會(huì)強(qiáng)制地調(diào)用GC線程,以便回收內(nèi)存用于新的分配。若GC一次之后仍不能滿足內(nèi)存分配的要求,JVM會(huì)再進(jìn)行兩次GC作進(jìn)一步的嘗試,若仍無(wú)法滿足要求,則 JVM將報(bào)“out of memory”的錯(cuò)誤,Java應(yīng)用將停止吱窝。

? ? 3)在編譯過(guò)程中作為一種優(yōu)化技術(shù)讥邻,Java 編譯器能選擇給實(shí)例賦 null 值,從而標(biāo)記實(shí)例為可回收院峡。

? ? 由于是否進(jìn)行主GC由JVM根據(jù)系統(tǒng)環(huán)境決定,而系統(tǒng)環(huán)境在不斷的變化當(dāng)中,所以主GC的運(yùn)行具有不確定性,無(wú)法預(yù)計(jì)它何時(shí)必然出現(xiàn),但可以確定的是對(duì)一個(gè)長(zhǎng)期運(yùn)行的應(yīng)用來(lái)說(shuō),其主GC是反復(fù)進(jìn)行的兴使。

7.3 ?減少GC開(kāi)銷的措施

? ? 根據(jù)上述GC的機(jī)制,程序的運(yùn)行會(huì)直接影響系統(tǒng)環(huán)境的變化,從而影響GC的觸發(fā)。若不針對(duì)GC的特點(diǎn)進(jìn)行設(shè)計(jì)和編碼,就會(huì)出現(xiàn)內(nèi)存駐留等一系列負(fù)面影響照激。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過(guò)程中的開(kāi)銷发魄。具體措施包括以下幾個(gè)方面:

(1)不要顯式調(diào)用System.gc()

? ? 此函數(shù)建議JVM進(jìn)行主GC,雖然只是建議而非一定,但很多情況下它會(huì)觸發(fā)主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數(shù)。

(2)盡量減少臨時(shí)對(duì)象的使用

? ? 臨時(shí)對(duì)象在跳出函數(shù)調(diào)用后,會(huì)成為垃圾,少用臨時(shí)變量就相當(dāng)于減少了垃圾的產(chǎn)生,從而延長(zhǎng)了出現(xiàn)上述第二個(gè)觸發(fā)條件出現(xiàn)的時(shí)間,減少了主GC的機(jī)會(huì)俩垃。

(3)對(duì)象不用時(shí)最好顯式置為Null

? ? 一般而言,為Null的對(duì)象都會(huì)被作為垃圾處理,所以將不用的對(duì)象顯式地設(shè)為Null,有利于GC收集器判定垃圾,從而提高了GC的效率励幼。

(4)盡量使用StringBuffer,而不用String來(lái)累加字符串

? ? 由于String是固定長(zhǎng)的字符串對(duì)象,累加String對(duì)象時(shí),并非在一個(gè)String對(duì)象中擴(kuò)增,而是重新創(chuàng)建新的String對(duì)象,如Str5=Str1+Str2+Str3+Str4,這條語(yǔ)句執(zhí)行過(guò)程中會(huì)產(chǎn)生多個(gè)垃圾對(duì)象,因?yàn)閷?duì)次作“+”操作時(shí)都必須創(chuàng)建新的String對(duì)象,但這些過(guò)渡對(duì)象對(duì)系統(tǒng)來(lái)說(shuō)是沒(méi)有實(shí)際意義的,只會(huì)增加更多的垃圾。避免這種情況可以改用StringBuffer來(lái)累加字符串,因StringBuffer是可變長(zhǎng)的,它在原有基礎(chǔ)上進(jìn)行擴(kuò)增,不會(huì)產(chǎn)生中間對(duì)象口柳。

(5)能用基本類型如Int,Long,就不用Integer,Long對(duì)象

? ? 基本類型變量占用的內(nèi)存資源比相應(yīng)對(duì)象占用的少得多,如果沒(méi)有必要,最好使用基本變量苹粟。

(6)盡量少用靜態(tài)對(duì)象變量

? ? 靜態(tài)變量屬于全局變量,不會(huì)被GC回收,它們會(huì)一直占用內(nèi)存。

(7)分散對(duì)象創(chuàng)建或刪除的時(shí)間

? ? 集中在短時(shí)間內(nèi)大量創(chuàng)建新對(duì)象,特別是大對(duì)象,會(huì)導(dǎo)致突然需要大量?jī)?nèi)存,JVM在面臨這種情況時(shí),只能進(jìn)行主GC,以回收內(nèi)存或整合內(nèi)存碎片,從而增加主GC的頻率跃闹。集中刪除對(duì)象,道理也是一樣的嵌削。它使得突然出現(xiàn)了大量的垃圾對(duì)象,空閑空間必然減少,從而大大增加了下一次創(chuàng)建新對(duì)象時(shí)強(qiáng)制主GC的機(jī)會(huì)。

7.4 ?關(guān)于垃圾回收的幾點(diǎn)補(bǔ)充

經(jīng)過(guò)上述的說(shuō)明望艺,可以發(fā)現(xiàn)垃圾回收有以下的幾個(gè)特點(diǎn):

? ? (1)垃圾收集發(fā)生的不可預(yù)知性:由于實(shí)現(xiàn)了不同的垃圾回收算法和采用了不同的收集機(jī)制苛秕,所以它有可能是定時(shí)發(fā)生,有可能是當(dāng)出現(xiàn)系統(tǒng)空閑CPU資源時(shí)發(fā)生找默,也有可能是和原始的垃圾收集一樣艇劫,等到內(nèi)存消耗出現(xiàn)極限時(shí)發(fā)生,這與垃圾收集器的選擇和具體的設(shè)置都有關(guān)系惩激。

? ? (2)垃圾收集的精確性:主要包括2 個(gè)方面:(a)垃圾收集器能夠精確標(biāo)記活著的對(duì)象店煞;(b)垃圾收集器能夠精確地定位對(duì)象之間的引用關(guān)系。前者是完全地回收所有廢棄對(duì)象的前提咧欣,否則就可能造成內(nèi)存泄漏浅缸。而后者則是實(shí)現(xiàn)歸并和復(fù)制等算法的必要條件。所有不可達(dá)對(duì)象都能夠可靠地得到回收魄咕,所有對(duì)象都能夠重新分配衩椒,允許對(duì)象的復(fù)制和對(duì)象內(nèi)存的縮并,這樣就有效地防止內(nèi)存的支離破碎哮兰。

? ? (3)現(xiàn)在有許多種不同的垃圾收集器毛萌,每種有其算法且其表現(xiàn)各異,既有當(dāng)垃圾收集開(kāi)始時(shí)就停止應(yīng)用程序的運(yùn)行喝滞,又有當(dāng)垃圾收集開(kāi)始時(shí)也允許應(yīng)用程序的線程運(yùn)行阁将,還有在同一時(shí)間垃圾收集多線程運(yùn)行。

? ? (4)垃圾收集的實(shí)現(xiàn)和具體的JVM 以及JVM的內(nèi)存模型有非常緊密的關(guān)系右遭。不同的JVM 可能采用不同的垃圾收集做盅,而JVM 的內(nèi)存模型決定著該JVM可以采用哪些類型垃圾收集$拖鳎現(xiàn)在,HotSpot 系列JVM中的內(nèi)存系統(tǒng)都采用先進(jìn)的面向?qū)ο蟮目蚣茉O(shè)計(jì)吹榴,這使得該系列JVM都可以采用最先進(jìn)的垃圾收集亭敢。

? ? (5)隨著技術(shù)的發(fā)展,現(xiàn)代垃圾收集技術(shù)提供許多可選的垃圾收集器图筹,而且在配置每種收集器的時(shí)候又可以設(shè)置不同的參數(shù)帅刀,這就使得根據(jù)不同的應(yīng)用環(huán)境獲得最優(yōu)的應(yīng)用性能成為可能。

針對(duì)以上特點(diǎn)远剩,我們?cè)谑褂玫臅r(shí)候要注意:

? ? (1)不要試圖去假定垃圾收集發(fā)生的時(shí)間扣溺,這一切都是未知的。比如瓜晤,方法中的一個(gè)臨時(shí)對(duì)象在方法調(diào)用完畢后就變成了無(wú)用對(duì)象锥余,這個(gè)時(shí)候它的內(nèi)存就可以被釋放。

? ? (2)Java中提供了一些和垃圾收集打交道的類活鹰,而且提供了一種強(qiáng)行執(zhí)行垃圾收集的方法--調(diào)用System.gc()哈恰,但這同樣是個(gè)不確定的方法。Java 中并不保證每次調(diào)用該方法就一定能夠啟動(dòng)垃圾收集志群,它只不過(guò)會(huì)向JVM發(fā)出這樣一個(gè)申請(qǐng)着绷,到底是否真正執(zhí)行垃圾收集,一切都是個(gè)未知數(shù)锌云。

? ? (3)挑選適合自己的垃圾收集器荠医。一般來(lái)說(shuō),如果系統(tǒng)沒(méi)有特殊和苛刻的性能要求桑涎,可以采用JVM的缺省選項(xiàng)彬向。否則可以考慮使用有針對(duì)性的垃圾收集器,比如增量收集器就比較適合實(shí)時(shí)性要求較高的系統(tǒng)之中攻冷。系統(tǒng)具有較高的配置娃胆,有比較多的閑置資源,可以考慮使用并行標(biāo)記/清除收集器等曼。

? ? (4)關(guān)鍵的也是難把握的問(wèn)題是內(nèi)存泄漏里烦。良好的編程習(xí)慣和嚴(yán)謹(jǐn)?shù)木幊虘B(tài)度永遠(yuǎn)是最重要的,不要讓自己的一個(gè)小錯(cuò)誤導(dǎo)致內(nèi)存出現(xiàn)大漏洞禁谦。

(5)盡早釋放無(wú)用對(duì)象的引用胁黑。大多數(shù)程序員在使用臨時(shí)變量的時(shí)候,都是讓引用變量在退出活動(dòng)域(scope)后州泊,自動(dòng)設(shè)置為null丧蘸,暗示垃圾收集器來(lái)收集該對(duì)象,還必須注意該引用的對(duì)象是否被監(jiān)聽(tīng)遥皂,如果有力喷,則要去掉監(jiān)聽(tīng)器刽漂,然后再賦空值。

8 ?補(bǔ)充:

8.1? Java內(nèi)存泄露

? ? ? (1)靜態(tài)集合類像HashMap冗懦、Vector等的使用最容易出現(xiàn)內(nèi)存泄露爽冕,這些靜態(tài)變量的生命周期和應(yīng)用程序一致仇祭,所有的對(duì)象Object也不能被釋放披蕉,因?yàn)樗麄円矊⒁恢北籚ector等應(yīng)用著。

Static Vector v = new Vector();

for (int i = 1; i<100; i++)

{

Object o = new Object();

v.add(o);

o = null;

}

? ? ? 在這個(gè)例子中乌奇,代碼棧中存在Vector 對(duì)象的引用 v 和 Object 對(duì)象的引用 o 没讲。在 For 循環(huán)中,我們不斷的生成新的對(duì)象礁苗,然后將其添加到 Vector 對(duì)象中爬凑,之后將 o 引用置空。問(wèn)題是當(dāng) o 引用被置空后试伙,如果發(fā)生 GC嘁信,我們創(chuàng)建的 Object 對(duì)象是否能夠被 GC 回收呢?答案是否定的疏叨。因?yàn)椋?GC 在跟蹤代碼棧中的引用時(shí)潘靖,會(huì)發(fā)現(xiàn) v 引用,而繼續(xù)往下跟蹤蚤蔓,就會(huì)發(fā)現(xiàn) v 引用指向的內(nèi)存空間中又存在指向 Object 對(duì)象的引用卦溢。也就是說(shuō)盡管o 引用已經(jīng)被置空,但是 Object 對(duì)象仍然存在其他的引用秀又,是可以被訪問(wèn)到的单寂,所以 GC 無(wú)法將其釋放掉。如果在此循環(huán)之后吐辙, Object 對(duì)象對(duì)程序已經(jīng)沒(méi)有任何作用宣决,那么我們就認(rèn)為此 Java 程序發(fā)生了內(nèi)存泄漏。

? ? ? (2)各種連接昏苏,數(shù)據(jù)庫(kù)連接惋嚎,網(wǎng)絡(luò)連接,IO連接等沒(méi)有顯示調(diào)用close關(guān)閉绩卤,不被GC回收導(dǎo)致內(nèi)存泄露瘫析。

? ? ? (3)監(jiān)聽(tīng)器的使用,在釋放對(duì)象的同時(shí)沒(méi)有相應(yīng)刪除監(jiān)聽(tīng)器的時(shí)候也可能導(dǎo)致內(nèi)存泄露救巷。

8.2? GC性能調(diào)優(yōu)

? ? ? Java虛擬機(jī)的內(nèi)存管理與垃圾收集是虛擬機(jī)結(jié)構(gòu)體系中最重要的組成部分壶熏,對(duì)程序(尤其服務(wù)器端)的性能和穩(wěn)定性有著非常重要的影響。性能調(diào)優(yōu)需要具體情況具體分析浦译,而且實(shí)際分析時(shí)可能需要考慮的方面很多棒假,這里僅就一些簡(jiǎn)單常用的情況作簡(jiǎn)要介紹溯职。

? ? ? 我們可以通過(guò)給Java虛擬機(jī)分配超大堆(前提是物理機(jī)的內(nèi)存足夠大)來(lái)提升服務(wù)器的響應(yīng)速度,但分配超大堆的前提是有把握把應(yīng)用程序的Full GC頻率控制得足夠低帽哑,因?yàn)橐淮蜦ull GC的時(shí)間造成比較長(zhǎng)時(shí)間的停頓谜酒。控制Full GC頻率的關(guān)鍵是保證應(yīng)用中絕大多數(shù)對(duì)象的生存周期不應(yīng)太長(zhǎng)妻枕,尤其不能產(chǎn)生批量的僻族、生命周期長(zhǎng)的大對(duì)象,這樣才能保證老年代的穩(wěn)定屡谐。

? ? ? Direct Memory在堆內(nèi)存外分配述么,而且二者均受限于物理機(jī)內(nèi)存,且成負(fù)相關(guān)關(guān)系愕掏。因此分配超大堆時(shí)度秘,如果用到了NIO機(jī)制分配使用了很多的Direct Memory,則有可能導(dǎo)致Direct Memory的OutOfMemoryError異常饵撑,這時(shí)可以通過(guò)-XX:MaxDirectMemorySize參數(shù)調(diào)整Direct Memory的大小剑梳。

? ? ? 除了Java堆和永久代以及直接內(nèi)存外,還要注意下面這些區(qū)域也會(huì)占用較多的內(nèi)存滑潘,這些內(nèi)存的總和會(huì)受到操作系統(tǒng)進(jìn)程最大內(nèi)存的限制:1垢乙、線程堆棧:可通過(guò)-Xss調(diào)整大小,內(nèi)存不足時(shí)拋出StackOverflowError(縱向無(wú)法分配众羡,即無(wú)法分配新的棧幀)或OutOfMemoryError(橫向無(wú)法分配侨赡,即無(wú)法建立新的線程)。

? ? ? Socket緩沖區(qū):每個(gè)Socket連接都有Receive和Send兩個(gè)緩沖區(qū)粱侣,分別占用大約37KB和25KB的內(nèi)存羊壹。如果無(wú)法分配,可能會(huì)拋出IOException:Too many open files異常齐婴。關(guān)于Socket緩沖區(qū)的詳細(xì)介紹參見(jiàn)我的Java網(wǎng)絡(luò)編程系列中深入剖析Socket的幾篇文章油猫。

? ? ? JNI代碼:如果代碼中使用了JNI調(diào)用本地庫(kù),那本地庫(kù)使用的內(nèi)存也不在堆中柠偶。

? ? ? 虛擬機(jī)和GC:虛擬機(jī)和GC的代碼執(zhí)行也要消耗一定的內(nèi)存情妖。

9 ?代碼分析垃圾回收過(guò)程

public class SlotGc{

? ? ? ? ? ? ? ?public static void main(String[] args){

? ? ? ? ? ? ? ? ? ? ? ? ? byte[] holder = new byte[32*1024*1024];

? ? ? ? ? ? ? ? ? ? ? ? ? ?System.gc();

? ? ? ? ? ? ? ? }

}

? ? ? 代碼很簡(jiǎn)單,就是向內(nèi)存中填充了32MB的數(shù)據(jù)诱担,然后通過(guò)虛擬機(jī)進(jìn)行垃圾收集毡证。在Javac編譯后,在終端執(zhí)行如下指令:java -verbose:gc SlotGc來(lái)查看垃圾收集的結(jié)果蔫仙,得到如下輸出信息:

[GC 208K->134K(5056K), 0.0017306 secs]

[Full GC 134K->134K(5056K), 0.0121194 secs]

[Full GC 32902K->32902K(37828K), 0.0094149 sec]

? ? ? 注意第三行料睛,“->”之前的數(shù)據(jù)表示垃圾回收前堆中存活對(duì)象所占用的內(nèi)存大小,“->”之后的數(shù)據(jù)表示垃圾回收堆中存活對(duì)象所占用的內(nèi)存大小,括號(hào)中的數(shù)據(jù)表示堆內(nèi)存的總?cè)萘浚?.0094149 sec 表示垃圾回收所用的時(shí)間恤煞。

? ? ? 從結(jié)果中可以看出屎勘,System.gc(()運(yùn)行后并沒(méi)有回收掉這32MB的內(nèi)存,這應(yīng)該是意料之中的結(jié)果居扒,因?yàn)樽兞縣older還處在作用域內(nèi)概漱,虛擬機(jī)自然不會(huì)回收掉holder引用的對(duì)象所占用的內(nèi)存。

修改代碼如下:

public class SlotGc{

? ? ? ? ? ? ? ? ? ? public static void main(String[] args){

? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? byte[] holder = new byte[32*1024*1024];

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? System.gc();

? ? ? ? ? ?}

}

? ? ? 加入花括號(hào)后喜喂,holder的作用域被限制在了花括號(hào)之內(nèi)瓤摧,因此,在執(zhí)行System.gc()時(shí)夜惭,holder引用已經(jīng)不能再被訪問(wèn)姻灶,邏輯上來(lái)講,這次應(yīng)該會(huì)回收掉holder引用的對(duì)象所占的內(nèi)存诈茧。但查看垃圾回收情況時(shí),輸出信息如下:

[GC 208K->134K(5056K), 0.0017100 secs]

[Full GC 134K->134K(5056K), 0.0125887 secs]

[Full GC 32902K->32902K(37828K), 0.0089226 secs]

? ? ? 很明顯捂掰,這32MB的數(shù)據(jù)并沒(méi)有被回收敢会。下面我們?cè)僮鋈缦滦薷模?/p>

public class SlotGc{

? ? ? ? ? ? ? ? public static void main(String[] args){

? ? ? ? {

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? byte[] holder = new byte[32*1024*1024];

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? holder = null;

? ? ? ? ? ? ? ? ? ? ? ? ?}

? ? ? ? ? ? ? ? ? System.gc();

? ? ? ? }

}

這次得到的垃圾回收信息如下:

[GC 208K->134K(5056K), 0.0017194 secs]

[Full GC 134K->134K(5056K), 0.0124656 secs]

[Full GC 32902K->134K(37828K), 0.0091637 secs]

說(shuō)明這次holder引用的對(duì)象所占的內(nèi)存被回收了。

? ? ? 首先明確一點(diǎn):holder能否被回收的根本原因是局部變量表中的Slot是否還存有關(guān)于holder數(shù)組對(duì)象的引用这嚣。

? ? ? 在第一次修改中鸥昏,雖然在holder作用域之外進(jìn)行回收,但是在此之后姐帚,沒(méi)有對(duì)局部變量表的讀寫(xiě)操作吏垮,holder所占用的Slot還沒(méi)有被其他變量所復(fù)用。所以作為GC Roots一部分的局部變量表仍保持者對(duì)它的關(guān)聯(lián)罐旗。這種關(guān)聯(lián)沒(méi)有被及時(shí)打斷膳汪,因此GC收集器不會(huì)將holder引用的對(duì)象內(nèi)存回收掉。 在第二次修改中九秀,在GC收集器工作前遗嗽,手動(dòng)將holder設(shè)置為null值,就把holder所占用的局部變量表中的Slot清空了鼓蜒,因此痹换,這次GC收集器工作時(shí)將holder之前引用的對(duì)象內(nèi)存回收掉了。

? ? ? 當(dāng)然都弹,我們也可以用其他方法來(lái)將holder引用的對(duì)象內(nèi)存回收掉娇豫,只要復(fù)用holder所占用的slot即可,比如在holder作用域之外執(zhí)行一次讀寫(xiě)操作畅厢。

? ? ? 為對(duì)象賦null值并不是控制變量回收的最好方法冯痢,以恰當(dāng)?shù)淖兞孔饔糜騺?lái)控制變量回收時(shí)間才是最優(yōu)雅的解決辦法。另外,賦null值的操作在經(jīng)過(guò)虛擬機(jī)JIT編譯器優(yōu)化后會(huì)被消除掉系羞,經(jīng)過(guò)JIT編譯后郭计,System.gc()執(zhí)行時(shí)就可以正確地回收掉內(nèi)存,而無(wú)需賦null值椒振。

參考文章:

深入理解 Java 垃圾回收機(jī)制 - Andy趙 - 博客園昭伸;

【深入Java虛擬機(jī)(8)】:Java垃圾收集機(jī)制 - ImportNew

Java GC系列(2):Java垃圾回收是如何工作的澎迎? - ImportNew庐杨;

GC算法基礎(chǔ) - ImportNew

Java垃圾回收機(jī)制 - ywl925 - 博客園夹供;

Java垃圾回收機(jī)制 - zsuguangh的專欄 - 博客頻道 - CSDN.NET灵份。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哮洽,隨后出現(xiàn)的幾起案子填渠,更是在濱河造成了極大的恐慌,老刑警劉巖鸟辅,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氛什,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡匪凉,警方通過(guò)查閱死者的電腦和手機(jī)枪眉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)再层,“玉大人贸铜,你說(shuō)我怎么就攤上這事∧羰埽” “怎么了蒿秦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)饺饭。 經(jīng)常有香客問(wèn)我渤早,道長(zhǎng),這世上最難降的妖魔是什么瘫俊? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任鹊杖,我火速辦了婚禮,結(jié)果婚禮上扛芽,老公的妹妹穿的比我還像新娘骂蓖。我一直安慰自己,他們只是感情好川尖,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布登下。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪被芳。 梳的紋絲不亂的頭發(fā)上缰贝,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音畔濒,去河邊找鬼剩晴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛侵状,可吹牛的內(nèi)容都是我干的赞弥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼趣兄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼绽左!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起艇潭,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拼窥,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后暴区,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闯团,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年仙粱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彻舰。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伐割,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刃唤,到底是詐尸還是另有隱情隔心,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布尚胞,位于F島的核電站硬霍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏笼裳。R本人自食惡果不足惜唯卖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躬柬。 院中可真熱鬧拜轨,春花似錦、人聲如沸允青。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至法牲,卻和暖如春史汗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拒垃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工停撞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恶复。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓怜森,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谤牡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子副硅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容