大部分人會把垃圾回收(Garbage Collection,GC)當作Java的伴生產物,事實上,GC的歷史要比Java久遠。java
在堆裏存放着Java中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事就是要確認哪些對象還「存活」着,哪些已經「死去」(不可能再被使用)。算法
引用計數算法(Reference Counting):給對象添加一個引用計數器,每當有一個地方引用它,計數器就+1,引用失效,計數器-1:任什麼時候刻計數器爲0的對象就是不能再被使用的。數組
這種算法實現簡單,斷定效率也很高,但它難以解決對象之間相互循環引用的問題,若是兩個對象中存在互相引用,就沒法通知GC收集器回收它們。Java虛擬機並非經過引用計數算法來判斷對象是否存活的。緩存
在主流商用程序語言(Java、C#)的主流實現中,都是經過可達性分析(Reachability Analysis)來斷定對象是否存活的。基本思路是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。安全
Java中,可做爲GC Roots的對象包括如下幾種:多線程
不管是引用計數算法仍是可達性分析算法,判斷對象是否存活都與「引用」有關,在JDK1.2以前,Java中的引用的定義:若是reference類型的數據存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹但太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用這兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力,咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。併發
JDK1.2以後Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用4中(Phantom Reference),這4種引用強度依次逐漸減弱。jvm
Object obj = new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。 即便在可達性分析算法中不可達的對象,也並不是是「非死不可」,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象的死亡,至少要經歷兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選條件時此對象是否有必要執行finalize()
方法。當對象沒有覆蓋finalize()
方法,或者finalize()
方法已經被jvm調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。this
若是這個對象被斷定爲有必要執行finalize方法,那麼這個對象會放置在F-Queue隊列中,並在稍後由一個由虛擬機自動創建的、低優先級的Finalizer線程去執行它(調用finalize方法)。執行是指觸發這個方法,但不承諾會等待它運行結束,由於若是一個對象在F-Queue中執行緩慢,或者發生了死循環,極可能致使其餘對象永久處於等待狀態,甚至整個內存回收系統崩潰。稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要再finalize()中拯救本身——只須要從新與引用鏈上任意一個對象創建關聯,譬如把本身(this)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。線程
不少人認爲方法區是沒有垃圾收集的,Java虛擬機規範中確實說過不要求虛擬機在方法區實現垃圾收集,並且在方法區進行垃圾收集的性價比很低:在堆中,尤爲是新生代,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而方法區的垃圾收集效率遠低於此。
方法區的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是相似。假如當前系統中沒有一個對象引用了當前常量池中的某個常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
判斷一個類是否無用的條件比判斷常量苛刻的多,類須要知足如下三個條件,纔會被斷定爲「無用的類」
算法分爲「標記」和"清除"(Mark-Sweep)兩個階段,首先標記出須要回收的對象,在標記完成後統一回收。後續的收集算法都基於這種思路並對其不足進行改進。
缺點:一是效率問題,標記和清除兩個階段的效率都不高;另外一個是空間問題,標記清楚後會產生大量不連續的內存碎片,空間碎片太多可能致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集。
爲了解決效率問題,一種稱爲"複製"(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊.當這塊內存用完了,就將還存活着的對象複製到另一塊上面,而後把已使用過的內存空間一次清理掉.這樣使得每次都是對整個半區進行內存回收,也不會出現內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單、高效。但代價是將內存縮小爲了原來的1/2。
商業虛擬機都採用這種手機算法來回收新生代。IBM研究代表,新生代中98%的對象都是「朝生夕死」,因此不須要1:1的比例來劃份內存,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中和存活的對象一次性複製到另一塊Survivor上,最後清理Eden和用過的Survivor空間。HotSpot默認Eden和Survivor大小比例爲8:1。也就是新生代中可用空間爲整個新生代的90%,只有10%會被「浪費」。當Survivor空間不夠用時,須要依賴其餘內存(老年代)進行分配擔保。
複製收集算法在對象存活率較高時要進行較多的複製操做,效率將會下降,更關鍵的在於若是不想浪費一半的內存空間,就須要有額外空間進行分配擔保,以應對被使用的內存中全部對象都是100%存活的極端狀況,因此在老年代通常不能直接使用這種算法。
根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程與「標記-清除」同樣,但後續步驟不是進行清理,而是讓全部存活的對象都向一端移動,而後清理掉端邊界之外的內存。
當前商業虛擬機的垃圾收集都採用「分代收集」(Generational Collection)算法,這種算法根據對象存活週期的不一樣,將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法,在新生代,每次垃圾收集都有大量的對象死去,只有少許存活,那就選用賦值算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代由於對象存活率高,沒有額外空間進行分配擔保,就必須使用「標記-清理」或者「標記-整理"算法進行回收。
從可達性分析中從GC Roots結點找引用鏈這個操做爲例,可做爲GC Roots的結點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。
另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行——這裏「一致性」的意思是在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,該點不知足的話分析結果準確性就沒法獲得保證。這是致使GC進行時必須停頓全部Java執行線程(Sun將這件事稱之爲「stop the world」)的其中一個重要緣由,即便是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是要停頓的。
在OopMap的協助下,HotSpot能夠快速且準確的完成GC Roots枚舉,但HotSpot並非爲每條指令都生成OopMap,只是在特定的位置記錄這些信息,這些位置稱爲"安全點",即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點是才能展廳。例如,方法調用、循環跳轉、異常跳轉等,這些功能的指令才能產生safepoint。
對於safepoint,另外一個須要考慮的問題是如何在GC發生時讓全部線程都跑到最近的安全點上在停下來。兩種方案供選擇:搶先式中斷(Preemptive Suspension)和主動式終端(Voluntary Suspension),其中搶先式中斷不須要線程的執行代碼主動配合,在GC發生時就首先把線程所有中斷,若是有中斷的地方不在安全點上,就回複線程,讓它跑到安全點上。幾乎沒有虛擬機實現採用搶先式中斷來展廳線程從而響應GC。而主動式的思想是GC須要中斷線程時,不直接對線程操做,僅簡單的設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身主動掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。
當線程處於Sleep或者Blocked狀態是,線程沒法響應JVM的中斷請求,走到安全點也沒法中斷掛起,JVM顯然不會等待線程後從新被分配CPU的時間,這種狀況就須要安全區域(Safe Region)解決。
安全區域指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的,咱們也能夠把Safe Region看作被擴展了的SafePoint。
在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者整個GC過程),若是完成了,那線程就繼續執行,不然就必須等到直到收到能夠安全離開safe region的信號爲止。
1.Serial收集器:新生代的「單線程」的收集器
2.ParNew收集器:Serial收集器的多線程版本
3.Parallel Scavenge收集器:新生代收集器,也是使用複製算法的收集器,又是並行的多線程收集器。
4.Serial Old收集器:Serial收集器的老年代版本
5.Parallel Old收集器:Parallel Scavenge收集器的老年代版本。使用多線程和「標記-整理」算法,jdk1.6開始提供
6.CMS收集器:一種獲取最短回收停頓事件爲目標的收集器。基於「標記-清除」算法。
7.G1收集器:當今收集器技術發展的最前沿成果,具備特色:並行與併發,分代收集,空間整合,可預測的停頓
對象的內存分配從大方向講,就是在堆上分配,對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配,少數狀況下也有可能直接分配在老年代中,分配的規則並不是百分之百固定的,細節取決於當前使用的是哪種垃圾收集器的組合,還有虛擬機中與內存相關的參數設置。
大多數狀況下,對象在新生代的Eden區分配,當Eden區沒有足夠空間時,虛擬機將發起一次Minor GC。
所謂大對象是指須要大量連續內存空間的對象,最典型的大對象就是那種很長的字符串以及數組。大對象對虛擬機的內存分配來講就是一個壞消息,常常出現大對象容易致使內存還有很多空間時就提早觸發GC以獲取足夠的連續空間來「安置」它們。
虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且可以被Survivor容納,將被移動到Survivor空間中,而且對象年齡爲1.對象在Survivor區每「熬過」一次Minor GC,年齡就增長一歲,當年齡增長到必定程度(默認爲15歲),就將被晉升到老年代中。對象晉升老年代年齡的閾值,能夠經過參數設置。
若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就能夠直接進入老年代,無需等到要求的年齡
發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,若是成立,那麼Minor GC確保是安全的,若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試進行一次Minor GC,儘管是有風險的,若是小於或者設置不容許冒險,那這時也要進行一次Full GC。