程序計數器,虛擬機棧,本地方法棧3個區域隨線程而生,隨線程而滅。在這幾個區域內就不須要過多考慮回收的問題,由於方法結束或者線程結束,內存天然就跟着回收了。而Java
堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的也是這部份內存。java
垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還存活着,哪些已經死去(即不可能再被任何途徑使用的對象)。算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失敗時,計數器值就減1。任什麼時候刻計數器爲0的對象就是不可能再被使用的。編程
優勢:實現簡單,斷定效率也很高;數組
缺點:很難解決對象之間相互循環引用的問題,這也是主流的Java
虛擬機裏面沒有選用引用計數算法來管理內存的緣由;緩存
/** * 對象的循環引用 */ public class CircularReference { Object instance; public static void main(String[] args) { CircularReference reference1 = new CircularReference(); CircularReference reference2 = new CircularReference(); reference1.instance = reference2; reference2.instance = reference1; } }
可達性分析(Reachability Analysis
)經過一系列的被稱爲GC ROOTS
的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain
),當一個對象到GC ROOTS
沒有任何引用鏈相連時,則證實此對象是不可用的。安全
GC ROOTS
的對象JNI
(即通常所說的Native
方法)引用的對象;真正宣告一個對象死亡,至少要經歷兩次標記過程:多線程
若是對象在進行可達性分析後發現沒有與GC ROOTS
相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()
方法。當對象沒有覆蓋finalize()
方法,或者finalize()
方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行。併發
若是這個對象被斷定爲有必要執行finalize()
方法,對象須要在finalize()
方法中從新與引用鏈上的任何一個對象創建關聯便可。若是對象沒有作此操做,那麼將對其進行第二次標記,它就真的被回收了。jvm
finalize()
方法嗎 任何一個對象的finalize()
方法都只會被系統自動調用一次。ide
finalize()
方法性能如何 finalize()
方法的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。finalize()
能作的全部工做,使用try-finally
或者其餘方式均可以作得更好,更及時。所以應儘可能避免使用它。
在JDK
1.2之前,Java
中的引用的定義很傳統:若是reference
類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。在JDK
1.2以後,將引用分爲強引用(Strong Reference
),軟引用(Soft Reference
),弱引用(Weak Reference
),虛引用(Phantom Reference
)4種,這4種引用強度依次逐漸減弱。
強引用:是指在程序代碼之中廣泛存在的,相似Object obj = new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象;
軟引用:描述一些還有用但非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常;
弱引用:描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象;
虛引用:它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知;
Java
爲何擴充了引用的概念咱們但願能描述這樣一類對象:當內存空間還足夠時,能保留在內存之中。若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。
方法區的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。
回收廢棄常量與回收Java
堆中的對象很是相似。以常量池中字面量的回收爲例,假如一個字符串"abc"
已經進入了常量池中,可是當前系統沒有任何String
對象引用常量池中的"abc"
常量,也沒有其餘地方引用了這個字面量,若是這時候發生內存回收,並且必要的話,這個"abc"
變量就會被系統清理出常量池。常量池中的其餘類(接口),方法,字段的符號引用也與此相似。
Java
堆中不存在該類的任何實例;ClassLoader
已經被回收;java.lang.Class
對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法;虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是能夠,而並非和對象同樣,不使用了就必然會回收。
標記-清除(Mark-Sweep
)算法分爲標記和清除兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。它是最基礎的收集算法,由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。
複製(Copying
)算法爲了解決效率問題,將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣也就不用考慮內存碎片等複雜狀況。
優勢:實現簡單,運行高效;
缺點:將內存縮小爲原來的一半,代價有點大;
標記-整理(Mark-Compact
)算法標記過程仍然與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。
當前商業虛擬機的垃圾收集都採用分代收集(Generation Collection
)算法,根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java
堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清除或標記-整理算法來進行回收。
若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java
虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商,不一樣版本的虛擬機所提供的垃圾收集器均可能有很大差異。
Serial
收集器 Serial
收集器是最基本,發展歷史最悠久的收集器,這個收集器是一個單線程的收集器。它在進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束(Stop The World
)。
跟其它單線程的收集器相比,它有着簡單而高效的優勢。
ParNew
收集器 ParNew
收集器其實就是Serial
收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲都與Serial
收集器徹底同樣。
ParNew
收集器在單CPU
的環境中絕對不會有比Serial
收集器更好的效果,固然隨着可使用的CPU
數量增長,它對於GC
時系統的有效利用仍是頗有好處的。
併發和並行,這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們能夠解釋以下:
Parallel
):指多條垃圾收集線程並行工做,但此時用戶線程仍處於等待狀態;Concurrent
):指用戶線稱與垃圾收集線程同時執行(但並不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU
上。Parallel Scavenge
收集器 Parallel Scavenge
收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。
CMS
等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge
收集器的目標則是達到一個可控制的吞吐量(Throughput
)。所謂吞吐量就是CPU
用於運行用戶代碼的時間與CPU
總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉一分鐘,那吞吐量就是99%
。
停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU
時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
Serial Old
收集器 Serial Old
是Serial
收集器的老年代版本,它一樣是一個單線程收集器,使用標記-整理算法。它主要有兩大用途:一種用途是在JDK1.5
以及之前的版本中與Parallel Scavenge
收集器搭配使用,另外一種用途是做爲CMS
收集器的後備方案。
Parallel Old
收集器 Parallel Old
是Parallel Scavenge
收集器的老年代版本,使用多線程和標記-整理算法。
在注重吞吐量以及CPU
資源敏感的場合,均可以優先考慮Parallel Scavenge
加Parallel Old
收集器。
CMS
收集器 CMS
(Concurrent Mark Sweep
)收集器是一種以獲取最短回收停頓時間爲目標的收集器,基於標記-清除算法。CMS
收集器是HotSpot
虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。目前很大一部分的Java
應用集中在互聯網或者B/S
系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS
收集器就很是符合這類應用的需求。
CMS
收集器收集過程分爲4個步驟,包括:
CMS initial mark
)CMS concurrent mark
)CMS remark
)CMS concurrent sweep
) 初始標記,從新標記這兩個步驟仍然須要Stop The World
。初始標記僅僅只是標記一下GC Roots
能直接關聯到的對象,速度很快,併發標記階段進行進行GC Roots Tracing
的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記稍長一些,但遠比並發標記的時間短。併發清除階段會開啓用戶線程,同時GC
線程開始對標記的區域作清掃。
優勢:併發收集,低停頓;
缺點:
CMS
收集器對CPU
資源很是敏感;CMS
收集器沒法處理浮動垃圾(Floating Garbage
);CMS
因爲是基於標記-清除算法實現的收集器,這意味着收集結束時會有大量空間碎片產生;G1
收集器 G1(Garbage-First)
是一款面向服務端應用的垃圾收集器,主要針對配備多顆處理器及大容器內存的機器,以極高機率知足GC
停頓時間要求的同時,還具有高吞吐量性能特徵。
與其它GC
收集器相比,G1
具有以下特色:
G1
能充分利用CPU
,多核環境下的硬件優點,使用多個CPU
來縮短STW
);G1
收集器可以管理整個GC
堆,但保留了分代收集的概念);G1
從總體上看是基於標記-整理算法實現的,從局部上看是基於複製算法); G1
跟蹤各個Region
裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region
(這也就是Garbage-First
名稱的來由)。這種使用Region
劃份內存空間以及有優先級的區域回收方式,保證了G1
收集器在有限的時間內能夠獲取儘量高的收集效率。
G1
收集器的運做大體可分爲如下幾個步驟:
Initial Marking
);Concurrent Marking
);Final Marking
);Live Data Counting and Evacuation
); 初始標記階段僅僅是標記一下GC Roots
能直接關聯到的對象,這階段須要停頓線程,但耗時很短。併發標記階段是從GC Root
開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄。這階段須要停頓線程,可是可並行執行。篩選回收階段首先對各個Region
的回收價值和成本進行排序,根據用戶所指望的GC
停頓時間來制定回收計劃。
HotSpot
爲何要分爲新生代和老年代 根據對象存活週期的不一樣將內存分爲幾塊。通常將java
堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。好比在新生代中,每次收集都會有大量對象死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇標記-清除或標記-整理算法進行垃圾收集。
GC
看堆空間的結構 從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此Java
堆還能夠細分爲:新生代和老年代。再細緻一點有:Eden
空間,From Survivor(s0)
,To Survivor(s1)
,Tentired
空間。
eden
,s0
,s1
區都屬於新生代,tentired
區屬於老年代。在大部分狀況下,對象都會首先在Eden
區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入s1("To")
,而且對象的年齡還會加1(Eden
->Survivor
後對象的初始年齡變爲1),當它的年齡增長到必定程度(默認爲15歲),就會被晉升到老年代中。通過此次GC
後,Eden
和From
已經被清空。這個時候,From
和To
會交換他們的角色,也就是新的To
就是上次GC
前的From
,新的From
就是上次GC
前的To
。無論怎樣,都會保證名爲To
的Survivor
區域是空的。Minor GC
會一直重複這樣的過程,直到To
區被填滿,To
區被填滿以後,會將全部對象移動到年老代中。
Eden
分配 大多數狀況下,對象在新生代Eden
區中分配。當Eden
區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC
。
Minor GC
和Full GC
有什麼不同嗎?
新生代GC
(Minor GC
):指發生在新生代的垃圾收集動做,由於Java
對象大多數都具有朝生夕滅的特性,因此Minor GC
很是頻繁,通常回收速度也比較快;
老年代GC
(Major GC
/Full GC
):指發生在老年代的GC
,出現了Major GC
,常常會伴隨至少一次的Minor GC
;
大對象是指,須要大量連續內存空間的Java
對象,最典型的大對象就是那種很長的字符串以及數組。
爲了不爲大對象分配內存時因爲分配擔保機制帶來的複製而下降效率
虛擬機給每一個對象定義了一個對象年齡(Age
)計數器。若是對象在Eden
出生並通過第一次Minor GC
後仍然存活,而且能被Survivor
容納的話,將被移動到Survivor
空間中,而且對象年齡設爲1。對象在Survivor
區中每熬過一次Minor GC
,年齡就增長一歲,但它的年齡增長到必定程度(默認爲15歲),就會被晉升到老年代中。
爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold
才能晉升老年代,若是在Survivor
空間中相同年齡全部對象大小的總和大於Survivor
空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代,無須等到MaxTenuringThreshold
中要求的年齡。
在發生Minor GC
以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC
能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure
設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC
,儘管此次Minor GC
是有風險的;若是小於,或者HandlePromotionFailure
設置不容許冒險,那這時也要改成進行一次Full GC
。