哪些內(nèi)存需要回收

在Java堆中存放著幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要知道哪些對(duì)象還“存活著”,哪些對(duì)象已經(jīng)”死去“。

引用計(jì)數(shù)算法

引用計(jì)數(shù)法的實(shí)現(xiàn):給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器就加1,當(dāng)引用失效時(shí),計(jì)數(shù)器就減1,只要計(jì)數(shù)器為0的對(duì)象就是不可能被使用的。

這個(gè)算法實(shí)現(xiàn)簡(jiǎn)單,效率也很高,但是當(dāng)存活對(duì)象中,存在相互引用的時(shí)候,這算法就解決不了。所以Java中的GC并沒(méi)有采用引用計(jì)數(shù)法來(lái)管理內(nèi)存。(后面例子分析會(huì)根據(jù)GC日志看出相互引用的對(duì)象被回收了)

可達(dá)性分析算法

以GC Roots對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)依次向下搜索,如果當(dāng)前對(duì)象到GC Roots沒(méi)有任何的路徑相連(對(duì)象不可達(dá))時(shí),那么,當(dāng)前對(duì)象沒(méi)有引用。

在Java中,以下對(duì)象可作為GC Roots:

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

  2. 本地方法棧中引用的對(duì)象;

  3. 方法區(qū)中的常量引用的對(duì)象;

  4. 方法區(qū)中的靜態(tài)屬性引用的對(duì)象。

當(dāng)這些對(duì)象不可達(dá)的時(shí)候,也并不是就可以宣告這個(gè)對(duì)象就死亡了。還有對(duì)象的最后一次自我救贖——finalize。

finalize是一個(gè)方法名,Java允許使用finalize方法在垃圾收集器將對(duì)象從內(nèi)存中清除出去之前做必要的清理工作。在這個(gè)操作中如果對(duì)象被重新引用,對(duì)象就可以活過(guò)來(lái)了。

判斷對(duì)象是否有必要執(zhí)行該方法主要有以下兩個(gè)依據(jù):

  • 對(duì)象有沒(méi)有覆蓋finalize方法;

  • 對(duì)象已覆蓋finalize方法,檢查finalize方法是否被虛擬機(jī)調(diào)用過(guò),如果已被調(diào)用,就不需要再次執(zhí)行。

如果該對(duì)象有必要執(zhí)行finalize方法,則該被對(duì)象將被放置到F-Queue中,由虛擬機(jī)的單獨(dú)線(xiàn)程Finalizer執(zhí)行。

注意:finalize方法只能被執(zhí)行一次,如果面臨下一次回收,finalize方法將不會(huì)被執(zhí)行。

何時(shí)回收

young gc

對(duì)于 young gc,觸發(fā)條件似乎要簡(jiǎn)單很多,當(dāng) eden 區(qū)的內(nèi)存不夠時(shí),就會(huì)觸發(fā)young gc。

full gc

1. old gen 空間不足

當(dāng)創(chuàng)建一個(gè)大對(duì)象、大數(shù)組時(shí),eden 區(qū)不足以分配這么大的空間,會(huì)嘗試在old gen 中分配,如果這時(shí) old gen 空間也不足時(shí),會(huì)觸發(fā) full gc,為了避免上述導(dǎo)致的 full gc,調(diào)優(yōu)時(shí)應(yīng)盡量讓對(duì)象在 young gc 時(shí)就能夠被回收,還有不要?jiǎng)?chuàng)建過(guò)大的對(duì)象和數(shù)組。

2. 統(tǒng)計(jì)得到的 young gc 晉升到 old gen的對(duì)象平均總大小大于old gen 的剩余空間

當(dāng)準(zhǔn)備觸發(fā)一次 young gc時(shí),會(huì)判斷這次 young gc 是否安全,這里所謂的安全是當(dāng)前老年代的剩余空間可以容納之前 young gc 晉升對(duì)象的平均大小,或者可以容納 young gen 的全部對(duì)象,如果結(jié)果是不安全的,就不會(huì)執(zhí)行這次 young gc,轉(zhuǎn)而執(zhí)行一次 full gc

3. perm gen 空間不足

如果有perm gen的話(huà),當(dāng)系統(tǒng)中要加載的類(lèi)、反射的類(lèi)和調(diào)用的方法較多,而且perm gen沒(méi)有足夠空間時(shí),也會(huì)觸發(fā)一次 full gc

4. ygc出現(xiàn) promotion failure

promotion failure 發(fā)生在 young gc 階段,即 cms 的 ParNewGC,當(dāng)對(duì)象的gc年齡達(dá)到閾值時(shí),或者 eden 的 to 區(qū)放不下時(shí),會(huì)把該對(duì)象復(fù)制到 old gen,如果 old gen 空間不足時(shí),會(huì)發(fā)生 promotion failure,并接下去觸發(fā)full gc

如何回收

標(biāo)記-清除算法

標(biāo)記清除算法分為”標(biāo)記“和”清除“兩個(gè)階段:首先標(biāo)記處所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。

它的不足有兩個(gè):

  1. 效率問(wèn)題,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高。

  2. 空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后的程序在運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

復(fù)制算法

為了解決效率問(wèn)題,”復(fù)制“算法出現(xiàn)了,將可用的內(nèi)存劃分為兩塊,每次只使用其中一塊, 當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這樣就不用考慮內(nèi)存碎片等復(fù)雜情況。

現(xiàn)在的商業(yè)虛擬機(jī)都采用這這種收集算法來(lái)回收新生代。根據(jù)研究表明,新生代中98%的對(duì)象都是”朝生夕死“的,所以新生代中將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間(from和to),每次使用Eden和其中一塊Survivor,當(dāng)回收時(shí),將Eden和剛才用過(guò)的Survivor中還存活的對(duì)象一次性的復(fù)制到另外一個(gè)Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor空間。

Hotspot虛擬機(jī)默認(rèn)使用Eden和Survivor的大小比例是8:1,也就是Eden占8,form和to各占1。

當(dāng)Survivor空間不夠的時(shí)候,需要依賴(lài)?yán)夏甏M(jìn)行分配擔(dān)保。

在發(fā)生Minor GC之前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象的總空間,如果大于,則此次Minor GC是安全的;如果小于,則虛擬機(jī)會(huì)查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果HandlePromotionFailure=true,那么會(huì)繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升到老年代的對(duì)象的平均大小,如果大于,則嘗試進(jìn)行一次Minor GC,但這次Minor GC依然是有風(fēng)險(xiǎn)的;如果小于或者HandlePromotionFailure=false,則改為進(jìn)行一次Full GC。

標(biāo)記-整理算法

標(biāo)記整理算法的“標(biāo)記”過(guò)程和標(biāo)記-清除算法一致,只是后面并不是直接對(duì)可回收對(duì)象進(jìn)行整理,而是讓所有存活的對(duì)象都向一段移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

分代收集算法

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用”分代收集“算法,其主要思想是將Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適合的收集算法。

在新生代采用復(fù)制算法,上面已經(jīng)講過(guò)了。而老年代因?yàn)閷?duì)象存活率高,沒(méi)有額外空間為它進(jìn)行分配擔(dān)保,就必須使用”標(biāo)記清理“或者”標(biāo)記整理“算法來(lái)進(jìn)行回收。

例子分析(查看GC日志)

GC日志是一個(gè)很重要的工具,它準(zhǔn)確記錄了每一次的GC的執(zhí)行時(shí)間和執(zhí)行結(jié)果,通過(guò)分析GC日志可以?xún)?yōu)化堆設(shè)置和GC設(shè)置,或者改進(jìn)應(yīng)用程序的對(duì)象分配模式。

JVM的GC日志的主要參數(shù)包括如下幾個(gè):

  • -XX:+PrintGC 輸出GC日志

  • -XX:+PrintGCDetails 輸出GC的詳細(xì)日志

  • -XX:+PrintGCTimeStamps 輸出GC的時(shí)間戳(以基準(zhǔn)時(shí)間的形式)

  • -XX:+PrintGCDateStamps 輸出GC的時(shí)間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

  • -XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息

  • -Xloggc:../logs/gc.log 日志文件的輸出路徑

引用計(jì)數(shù)算法

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 main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;        //假設(shè)在這行發(fā)生了GC,objA和ojbB是否被回收
        System.gc();
    }

}

設(shè)置JVM運(yùn)行參數(shù):

-XX:+PrintGCDetails-XX:+PrintHeapAtGC-XX:+PrintGCDateStamps-XX:+PrintTenuringDistribution-verbose:gc-Xloggc:gc.log

得到GC日志,我們分析當(dāng)調(diào)用System.gc()的時(shí)候,objA和ojbB是否被回收。

2017-07-01T12:23:12.844-0800: 0.268: [Full GC (System.gc()) [PSYoungGen: 624K->0K(38400K)] [ParOldGen: 8K->530K(87552K)] 632K->530K(125952K), [Metaspace: 3043K->3043K(1056768K)], 0.0059705 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap after GC invocations=2 (full 1): PSYoungGen      total 38400K, used 0K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 0% used [0x0000000795580000,0x0000000795580000,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
 ParOldGen       total 87552K, used 530K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x0000000740084858,0x0000000745580000)
 Metaspace       used 3043K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 336K, capacity 386K, committed 512K, reserved 1048576K}

從日志中[PSYoungGen: 624K->0K(38400K)]可以看出虛擬機(jī)并沒(méi)有因?yàn)閍和b互相引用就不回收它們,這也說(shuō)明虛擬機(jī)并不是通過(guò)引用計(jì)數(shù)算法來(lái)判斷對(duì)象是否存在引用的。

新生代MInor GC

通過(guò)此例可以分析JVM的內(nèi)存分配和回收策略:對(duì)象優(yōu)先在Eden分配。

public class TestAllocation {    private static final int _1MB = 1024 * 1024;    public static void testAllocation() {        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];  // 出現(xiàn)一次Minor GC
    }    public static void main(String[] args) {        testAllocation();
    }
}

設(shè)置JVM運(yùn)行參數(shù):

-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8

參數(shù)解釋?zhuān)涸谶\(yùn)行時(shí)通過(guò)-Xms20M、-Xmx20M、-Xmn10M這三個(gè)參數(shù)限制了Java堆的大小為20M,不可擴(kuò)展,其中10MB分配給新生代,剩下的10MB分配給老年代。XX:SurvivorRatio=8決定了新生代的中Eden區(qū)和一個(gè)Survivor區(qū)的空間比例為8:1。

運(yùn)行結(jié)果:

Heap
 PSYoungGen      total 9216K, used 7799K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 95% used [0x00000007bf600000,0x00000007bfd9dda0,0x00000007bfe00000)  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 4096K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)  object space 10240K, 40% used [0x00000007bec00000,0x00000007bf000010,0x00000007bf600000)
 Metaspace       used 3114K, capacity 4494K, committed 4864K, reserved 1056768K  class space    used 342K, capacity 386K, committed 512K, reserved 1048576K

執(zhí)行testAllocation()中分配allocation4對(duì)象的語(yǔ)句時(shí)會(huì)發(fā)生一次MinorGC,這次GC的結(jié)果是新生代6651KB變?yōu)?48KB,而總內(nèi)存占用量則幾乎沒(méi)有減少(因?yàn)閍llocation1、allocation2、allocation3三個(gè)對(duì)象都是存活的,虛擬機(jī)幾乎沒(méi)有占到可回收的對(duì)象)。這次GC發(fā)生的原因是給allocation4分配內(nèi)存的時(shí)候,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB,剩余空間已不足以分配allocation4所需的4MB內(nèi)存,一次發(fā)生了MInorGC。GC期間虛擬機(jī)又發(fā)現(xiàn)已有的3個(gè)2MB所需的對(duì)象全部無(wú)法放入Survivor空間(Survivor空間只有1MB大小),所以只能通過(guò)分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代中去。

所以這次GC的結(jié)果就是4MB的allocation4對(duì)象順利分配在Eden中,Survivor空閑,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。

調(diào)優(yōu)

GC優(yōu)化是迫不得已才采用的手段,多數(shù)導(dǎo)致GC問(wèn)題的Java應(yīng)用,都不是因?yàn)閰?shù)設(shè)置不當(dāng),而是代碼問(wèn)題。所以在實(shí)際使用中,分析GC情況優(yōu)化代碼比優(yōu)化GC參數(shù)要多得多。

GC優(yōu)化的目的有兩個(gè)

  1. 將轉(zhuǎn)移到老年代的對(duì)象數(shù)量降低到最?。?/p>

  2. 減少full GC的執(zhí)行時(shí)間;

為了達(dá)到上面的目的,一般地,你需要做的事情有:

  1. 減少使用全局變量和大對(duì)象;

  2. 調(diào)整新生代的大小到最合適;

  3. 設(shè)置老年代的大小為最合適;

  4. 選擇合適的GC收集器

http://www.cnblogs.com/aheizi/p/7105732.html