在Java堆中存放着幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要知道哪些對象還「存活着」,哪些對象已經」死去「。java
引用計數法的實現:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1,當引用失效時,計數器就減1,只要計數器爲0的對象就是不可能被使用的。算法
這個算法實現簡單,效率也很高,可是當存活對象中,存在相互引用的時候,這算法就解決不了。因此Java中的GC並無採用引用計數法來管理內存。(後面例子分析會根據GC日誌看出相互引用的對象被回收了)數組
以GC Roots對象做爲起始點,從這些節點依次向下搜索,若是當前對象到GC Roots沒有任何的路徑相連(對象不可達)時,那麼,當前對象沒有引用。安全
在Java中,如下對象可做爲GC Roots:工具
當這些對象不可達的時候,也並非就能夠宣告這個對象就死亡了。還有對象的最後一次自我救贖——finalize。優化
finalize是一個方法名,Java容許使用finalize方法在垃圾收集器將對象從內存中清除出去以前作必要的清理工做。在這個操做中若是對象被從新引用,對象就能夠活過來了。spa
判斷對象是否有必要執行該方法主要有如下兩個依據:線程
若是該對象有必要執行finalize方法,則該被對象將被放置到F-Queue中,由虛擬機的單獨線程Finalizer執行。日誌
注意:finalize方法只能被執行一次,若是面臨下一次回收,finalize方法將不會被執行。code
對於 young gc,觸發條件彷佛要簡單不少,當 eden 區的內存不夠時,就會觸發young gc。
當建立一個大對象、大數組時,eden 區不足以分配這麼大的空間,會嘗試在old gen 中分配,若是這時 old gen 空間也不足時,會觸發 full gc,爲了不上述致使的 full gc,調優時應儘可能讓對象在 young gc 時就可以被回收,還有不要建立過大的對象和數組。
當準備觸發一次 young gc時,會判斷此次 young gc 是否安全,這裏所謂的安全是當前老年代的剩餘空間能夠容納以前 young gc 晉升對象的平均大小,或者能夠容納 young gen 的所有對象,若是結果是不安全的,就不會執行此次 young gc,轉而執行一次 full gc
若是有perm gen的話,當系統中要加載的類、反射的類和調用的方法較多,並且perm gen沒有足夠空間時,也會觸發一次 full gc
promotion failure 發生在 young gc 階段,即 cms 的 ParNewGC,當對象的gc年齡達到閾值時,或者 eden 的 to 區放不下時,會把該對象複製到 old gen,若是 old gen 空間不足時,會發生 promotion failure,並接下去觸發full gc
標記清除算法分爲」標記「和」清除「兩個階段:首先標記處全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
它的不足有兩個:
爲了解決效率問題,」複製「算法出現了,將可用的內存劃分爲兩塊,每次只使用其中一塊, 當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。這樣就不用考慮內存碎片等複雜狀況。
如今的商業虛擬機都採用這這種收集算法來回收新生代。根據研究代表,新生代中98%的對象都是」朝生夕死「的,因此新生代中將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間(from和to),每次使用Eden和其中一塊Survivor,當回收時,將Eden和剛纔用過的Survivor中還存活的對象一次性的複製到另一個Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。
Hotspot虛擬機默認使用Eden和Survivor的大小比例是8:1,也就是Eden佔8,form和to各佔1。
當Survivor空間不夠的時候,須要依賴老年代進行分配擔保。
在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,若是大於,則這次Minor GC是安全的;若是小於,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小,若是大於,則嘗試進行一次Minor GC,但此次Minor GC依然是有風險的;若是小於或者HandlePromotionFailure=false,則改成進行一次Full GC。
標記整理算法的「標記」過程和標記-清除算法一致,只是後面並非直接對可回收對象進行整理,而是讓全部存活的對象都向一段移動,而後直接清理掉端邊界之外的內存。
當前商業虛擬機的垃圾收集都採用」分代收集「算法,其主要思想是將Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適合的收集算法。
在新生代採用複製算法,上面已經講過了。而老年代由於對象存活率高,沒有額外空間爲它進行分配擔保,就必須使用」標記清理「或者」標記整理「算法來進行回收。
GC日誌是一個很重要的工具,它準確記錄了每一次的GC的執行時間和執行結果,經過分析GC日誌能夠優化堆設置和GC設置,或者改進應用程序的對象分配模式。
JVM的GC日誌的主要參數包括以下幾個:
引用計數算法
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過 */ 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; //假設在這行發生了GC,objA和ojbB是否被回收 System.gc(); } }
設置JVM運行參數:
-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -verbose:gc -Xloggc:gc.log
獲得GC日誌,咱們分析當調用System.gc()的時候,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)]能夠看出虛擬機並無由於a和b互相引用就不回收它們,這也說明虛擬機並非經過引用計數算法來判斷對象是否存在引用的。
新生代MInor GC
經過此例能夠分析JVM的內存分配和回收策略:對象優先在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]; // 出現一次Minor GC } public static void main(String[] args) { testAllocation(); } }
設置JVM運行參數:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
參數解釋:在運行時經過-Xms20M、-Xmx20M、-Xmn10M這三個參數限制了Java堆的大小爲20M,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。XX:SurvivorRatio=8決定了新生代的中Eden區和一個Survivor區的空間比例爲8:1。
運行結果:
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
執行testAllocation()中分配allocation4對象的語句時會發生一次MinorGC,此次GC的結果是新生代6651KB變爲148KB,而總內存佔用量則幾乎沒有減小(由於allocation一、allocation二、allocation3三個對象都是存活的,虛擬機幾乎沒有佔到可回收的對象)。此次GC發生的緣由是給allocation4分配內存的時候,發現Eden已經被佔用了6MB,剩餘空間已不足以分配allocation4所需的4MB內存,一次發生了MInorGC。GC期間虛擬機又發現已有的3個2MB所需的對象所有沒法放入Survivor空間(Survivor空間只有1MB大小),因此只能經過分配擔保機制提早轉移到老年代中去。
因此此次GC的結果就是4MB的allocation4對象順利分配在Eden中,Survivor空閒,老年代被佔用6MB(被allocation一、allocation二、allocation3佔用)。
GC優化是無可奈何才採用的手段,多數致使GC問題的Java應用,都不是由於參數設置不當,而是代碼問題。因此在實際使用中,分析GC狀況優化代碼比優化GC參數要多得多。
GC優化的目的有兩個
爲了達到上面的目的,通常地,你須要作的事情有:
參考資料: