垃圾收集(Garbage Collection,GC),大部分人都把這項技術當作Java語言的伴生產物。事實上,GC的歷史比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC須要完成的3件事情:html
Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的(儘管在運行期會由JIT編譯器進行一些優化,但在本章基於概念模型的討論中,大致上能夠認爲是編譯期可知的),所以這幾個區域的內存分配和回收都具有肯定性,在這幾個區域內就不須要過多考慮回收的問題,由於方法結束或者線程結束時,內存天然就跟隨着回收了。而 Java堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存。java
在堆裏面存放着Java世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」。算法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任什麼時候候計數器爲 0 的對象就是不可能再被使用的。數組
這個方法實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最主要的緣由是它很難解決對象之間相互循環引用的問題。 所謂對象之間的相互引用問題,以下面代碼所示:除了對象 objA 和 objB 相互引用着對方以外,這兩個對象之間再無任何引用。可是他們由於互相引用對方,致使它們的引用計數器都不爲 0,因而引用計數算法(Reference Counting)沒法通知 GC 回收器回收他們。緩存
public class ReferenceCountingGc { Object instance = null; 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 Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。Java是經過可達性分析(Reachability Analysis)來斷定對象是否存活的。安全
在Java語言中,可做爲GC Roots的對象包含如下幾種:post
能夠理解爲:測試
不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在JDK 1.2之前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。優化
在JDK1.2以後,Java對引用的概念作了擴充,將引用分爲:url
這四種引用從上到下,依次減弱。
須要注意的是:在程序設計中通常不多使用弱引用與虛引用,使用軟引用的狀況較多,這是由於軟引用能夠加速 JVM 對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
即便在可達性分析法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑階段」,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過期,虛擬機將這兩種狀況視爲沒有必要執行。
被斷定爲須要執行的對象將會被放在一個叫作F-Queue的隊列之中進行第二次標記,除非這個對象與引用鏈上的任何一個對象創建關聯,不然就會被真的回收。
JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作「abc」的,換句話說,就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」:
虛擬機能夠對知足上述 3 個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息。
Java技術體系中的自動內存管理分爲對象內存回收和內存分配。這裏研究一下對象內存分配的問題。
對象的內存分配,往大方向講,就是在堆上分配(但也可能通過JIT編譯後被拆散爲標量類型並間接地棧上分配),對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。少數狀況下也可能會直接分配在老年代中,分配的規則並非百分之百固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。
回顧一下以前在 JVM探究之 —— Java內存區域 提到的Java堆:Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC 堆(Garbage Collected Heap)。從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分爲:新生代和老年代:再細緻一點,年輕代能夠劃分爲:Eden 空間、From Survivor (S0)、To Survivor (S1) 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
上圖所示的 eden 區、s0("From") 區、s1("To") 區都屬於新生代,tentired 區屬於老年代。大部分狀況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入 s1("To"),而且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold
來設置。通過此次GC後,Eden區和"From"區已經被清空。這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的「From」,新的"From"就是上次GC前的"To"。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,"To"區被填滿以後,會將全部對象移動到年老代中。
首先了解一下常見的 Minor GC和Full GC的區別:
在大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,而且在進程退出的時候輸出當前的內存各區域分配狀況。
/** * Eden區對象內存分配測試 * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails */ public class EdenAllocationTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1, allocation2,allocation3,allocation4; // allocation1=new byte[2*_1MB]; // allocation2=new byte[2*_1MB]; // allocation3=new byte[2*_1MB]; } }
如上面代碼中,在運行時經過-Xms20M、-Xmx20M、-Xmn10M這3個參數限制了Java堆大小爲20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8:1,從輸出的結果也能夠清晰地看到「eden space 8192K、from space 1024K、to space 1024K」的信息,新生代總可用空間爲9216KB(Eden區+1個Survivor區的總容量)。
分配兩個2MB的對象以後,空間使用狀況以下:
能夠看到,eden區新增使用空間 2*2*1MB(1024KB) = 4096KB,Eden區剩餘空間大小爲 8192KB-6620KB=1572KB < 2MB,此時再分配一個2MB的對象會怎麼樣呢?
能夠看到,當給allocation3對象分配內存時發生一次Minor GC,此次GC的結果是新生代6456KB變爲750KB,而總內存佔用量則幾乎沒有減小(由於allocation一、allocation2對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。此次GC發生的緣由是給allocation3分配內存的時候,剩餘空間已不足以分配allocation3所需的2MB內存,所以發生Minor GC。GC期間虛擬機又發現已有的2個2MB大小的對象所有沒法放入Survivor空間(Survivor空間只有1MB大小),因此只好經過分配擔保機制提早轉移到老年代去。老年代上的空間足夠存放 allocation1和allocation2對象,因此不會出現 Full GC。執行 Minor GC 後,後面分配的對象若是可以存在 eden 區的話,仍是會在 eden 區分配內存。
大對象是指,須要大量連續內存空間的Java對象,如:字符串以及數組。
虛擬機提供了一個 -XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。從而避免在Eden區及兩個Survivor區之間發生大量的內存複製。
/** * 大對象老年代分配測試 PretenureSizeThreshold * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728 * 說明: * -XX:+UseSerialGC 使用SerialGC * -XX:PretenureSizeThreshold=3145728 設置PretenureSizeThreshold爲3MB,這個參數不能像-Xmx之類的參數同樣直接寫3MB */ public class PretenureSizeThresholdTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1; allocation1=new byte[4*_1MB]; } }
須要注意的是:PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器通常並不須要設置。若是遇到必須使用此參數的場合,能夠考慮ParNew加CMS的收集器組合。下面示例代碼經過 -XX:UseSerialGC 來指定JVM使用Serial垃圾收集器演示。
從上面代碼中,能夠看出當給 allocation1對象分配空間時, Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了4MB,allocation1對象直接就分配在老年代中,這是由於PretenureSizeThreshold被設置爲3MB(就是3145728,這個參數不能像-Xmx之類的參數同樣直接寫3MB),所以超過3MB的對象都會直接在老年代進行分配。
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。爲了作到這點,虛擬機給每一個對象定義了一個對象年齡(Age)計數器。
若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每「熬過」一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置。
/** * 對象晉升老年代的年齡閾值測試 XX:MaxTenuringThreshold * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * 說明: * -XX:+UseSerialGC 使用SerialGC * -XX:MaxTenuringThreshold=1 年齡閾值,對象每熬過一次Minor GC,它的age會加1,age達到此值對象就會晉升老年代 * -XX:+PrintTenuringDistribution 輸出對象年齡 */ public class MaxTenuringThresholdTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1,allocation2,allocation3; allocation1=new byte[_1MB/4]; allocation2=new byte[4*_1MB]; allocation3=new byte[4*_1MB]; //首次GC allocation3=null; allocation3=new byte[4*_1MB]; //第二次GC } }
設置JVM參數-XX:MaxTenuringThreshold=1來查看執行結果:
能夠看出,上面執行結果中的allocation1對象須要256KB內存,在第一次GC時Survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後很是乾淨地變成0KB。
TODO:MaxTenuringThreshold=15
爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半(-XX:TargetSurvivorRatio=50 即:50%),年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
JVM究竟是如何來計算S區對象晉升到Old區的呢? 首先介紹另外一個重要的JVM參數: -XX:TargetSurvivorRatio
:一個計算指望s區存活大小(Desired survivor size)的參數。默認值爲50,即50%。 當一個S區中全部的age對象的大小若是大於等於Desired survivor size,則從新計算threshold,以age和MaxTenuringThreshold二者的最小值爲準。
在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,
上面提到了Minor GC依然會有風險,是由於新生代採用複製收集算法,假如大量對象在Minor GC後仍然存活(最極端狀況爲內存回收後新生代中全部對象均存活),而Survivor空間是比較小的,這時就須要老年代進行分配擔保,把Survivor沒法容納的對象放到老年代。老年代要進行空間分配擔保,前提是老年代得有足夠空間來容納這些對象,但一共有多少對象在內存回收後存活下來是不可預知的,所以只好取以前每次垃圾回收後晉升到老年代的對象大小的平均值做爲參考。使用這個平均值與老年代剩餘空間進行比較,來決定是否進行Full GC來讓老年代騰出更多空間。
取平均值仍然是一種機率性的事件,若是某次Minor GC後存活對象陡增,遠高於平均值的話,必然致使擔保失敗,若是出現了分配擔保失敗,就只能在失敗後從新發起一次Full GC。雖然存在發生這種狀況的機率,但大部分時候都是可以成功分配擔保的,這樣就避免了過於頻繁執行Full GC。