垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。算法
給每一個對象添加一個計數器,當有地方引用該對象時計數器加1,當引用失效時計數器減1。用對象計數器是否爲0來判斷對象是否可被回收。缺點:沒法解決循環引用的問題。數組
先建立一個字符串,String m = new String("jack");
,這時候 "jack" 有一個引用,就是m。而後將m設置爲null,這時候 "jack" 的引用次數就等於 0 了,在引用計數算法中,意味着這塊內容就須要被回收了。負載均衡
引用計數算法是將垃圾回收分攤到整個應用程序的運行當中了,而不是在進行垃圾收集時,要掛起整個應用的運行,直到對堆中全部對象的處理都結束。所以,採用引用計數的垃圾收集不屬於嚴格意義上的Stop-The-World
的垃圾收集機制。spa
看似很美好,但咱們知道JVM的垃圾回收就是Stop-The-World
的,那是什麼緣由致使咱們最終放棄了引用計數算法呢?看下面的例子。線程
public class ReferenceCountingGC { public Object instance; public ReferenceCountingGC(String name) { } public static void testGC(){ ReferenceCountingGC a = new ReferenceCountingGC("objA"); ReferenceCountingGC b = new ReferenceCountingGC("objB"); a.instance = b; b.instance = a; a = null; b = null; } }
咱們能夠看到,最後這2個對象已經不可能再被訪問了,但因爲他們相互引用着對方,致使它們的引用計數永遠都不會爲0,經過引用計數算法,也就永遠沒法通知GC收集器回收它們。3d
經過GC ROOT
的對象做爲搜索起始點,經過引用向下搜索,所走過的路徑稱爲引用鏈。經過對象是否有到達引用鏈的路徑來判斷對象是否可被回收(可做爲GC ROOT
的對象:虛擬機棧中引用的對象,方法區中類靜態屬性引用的對象,方法區中常量引用的對象,本地方法棧中JNI
引用的對象)code
經過可達性算法,成功解決了引用計數所沒法解決的循環依賴問題,只要你沒法與GC Root
創建直接或間接的鏈接,系統就會斷定你爲可回收對象。那這樣就引伸出了另外一個問題,哪些屬於GC Root。對象
Java內存區域中能夠做爲GC ROOT
的對象:blog
public class StackLocalParameter { public StackLocalParameter(String name) {} public static void testGC() { StackLocalParameter s = new StackLocalParameter("localParameter"); s = null; } }
此時的s,即爲GC Root,當s置空時,localParameter對象也斷掉了與GC Root的引用鏈,將被回收。
public class MethodAreaStaicProperties { public static MethodAreaStaicProperties m; public MethodAreaStaicProperties(String name) {} public static void testGC(){ MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties"); s.m = new MethodAreaStaicProperties("parameter"); s = null; } }
此時的s,即爲GC Root,s置爲null,通過GC後,s所指向的properties對象因爲沒法與GC Root創建關係被回收。而m做爲類的靜態屬性,也屬於GC Root,parameter 對象依然與GC root創建着鏈接,因此此時parameter對象並不會被回收。
public class MethodAreaStaicProperties { public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final"); public MethodAreaStaicProperties(String name) {} public static void testGC() { MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties"); s = null; } }
m即爲方法區中的常量引用,也爲GC Root,s置爲null後,final對象也不會因沒有與GC Root創建聯繫而被回收。
任何native接口都會使用某種本地方法棧,實現的本地方法接口是使用C鏈接模型的話,那麼它的本地方法棧就是C棧。當線程調用Java方法時,虛擬機會建立一個新的棧幀並壓入Java棧。然而當它調用的是本地方法時,虛擬機會保持Java棧不變,再也不在線程的Java棧中壓入新的幀,虛擬機只是簡單地動態鏈接並直接調用指定的本地方法。
在肯定了哪些垃圾能夠被回收後,垃圾收集器要作的事情就是開始進行垃圾回收,可是這裏面涉及到一個問題是:如何高效地進行垃圾回收。這裏咱們討論幾種常見的垃圾收集算法的核心思想。接口
標記清除算法(Mark-Sweep)是最基礎的一種垃圾回收算法,它分爲2部分,先把內存區域中的這些對象進行標記,哪些屬於可回收標記出來,而後把這些垃圾拎出來清理掉。就像上圖同樣,清理掉的垃圾就變成未使用的內存區域,等待被再次使用。但它存在一個很大的問題,那就是內存碎片。
上圖中等方塊的假設是2M,小一些的是1M,大一些的是4M。等咱們回收完,內存就會切成了不少段。咱們知道開闢內存空間時,須要的是連續的內存區域,這時候咱們須要一個2M的內存區域,其中有2個1M是無法用的。這樣就致使,其實咱們自己還有這麼多的內存的,但卻用不了。
複製算法(Copying)是在標記清除算法基礎上演化而來,解決標記清除算法的內存碎片問題。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。保證了內存的連續可用,內存分配時也就不用考慮內存碎片等複雜狀況。複製算法暴露了另外一個問題,例如硬盤原本有500G,但卻只能用200G,代價實在過高。
標記-整理算法標記過程仍然與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,再清理掉端邊界之外的內存區域。
標記整理算法解決了內存碎片的問題,也規避了複製算法只能利用一半內存區域的弊端。標記整理算法對內存變更更頻繁,須要整理全部存活對象的引用地址,在效率上比複製算法要差不少。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
分代收集算法分代收集算法嚴格來講並非一種思想或理論,而是融合上述3種基礎的算法思想,而產生的針對不一樣狀況所採用不一樣算法的一套組合拳,根據對象存活週期的不一樣將內存劃分爲幾塊。
對象的內存分配,往大方向講,就是在堆上分配(但也可能通過JIT
編譯後被拆散爲標量類型並間接地棧上分配),對象主要分配在新生代的Eden
區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB
上分配。少數狀況下也可能會直接分配在老年代中(大對象直接分到老年代),分配的規則並非百分百固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。
大多數狀況下,對象會在新生代Eden
區中分配。當Eden區沒有足夠空間進行分配時,虛擬機會發起一次 Minor GC
。Minor GC相比Major GC
更頻繁,回收速度也更快。經過Minor GC以後,Eden區中絕大部分對象會被回收,而那些存活對象,將會送到Survivor
的From區(若From區空間不夠,則直接進入Old區) 。
Survivor區至關因而Eden區和Old區的一個緩衝,相似於咱們交通燈中的黃燈。Survivor又分爲2個區,一個是From區,一個是To區。每次執行Minor GC
,會將Eden區中存活的對象放到Survivor的From區,而在From區中,仍存活的對象會根據他們的年齡值來決定去向。(From Survivor
和To Survivor
的邏輯關係會發生顛倒: From變To , To變From,目的是保證有連續的空間存放對方,避免碎片化的發生)
若是沒有Survivor區,Eden區每進行一次Minor GC
,存活的對象就會被送到老年代,老年代很快就會被填滿。而有不少對象雖然一次Minor GC
沒有消滅,但其實也並不會蹦躂多久,或許第二次,第三次就須要被清除。這時候移入老年區,很明顯不是一個明智的決定。因此,Survivor的存在乎義就是減小被送到老年代的對象,進而減小Major GC
的發生。Survivor的預篩選保證,只有經歷16次Minor GC
還能在新生代中存活的對象,纔會被送到老年代。
所謂大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。大對象對虛擬機的內存分配來講就是一個壞消息,常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來 「安置」 它們。
虛擬機提供了一個XX:PretenureSizeThreshold
參數,令大於這個設置值的對象直接在老年代分配,這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(新生代採用的是複製算法)。
虛擬機給每一個對象定義了一個對象年齡(Age)計數器,若是對象在Eden出生並通過第一次Minor GC
後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中(正常狀況下對象會不斷的在Survivor的From與To區之間移動),而且對象年齡設爲1。對象在Survivor區中每經歷一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認15歲),就將會晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數 XX:MaxPretenuringThreshold
設置。
爲了能更好地適應不一樣程度的內存情況,虛擬機並非永遠地要求對象的年齡必須達到 MaxPretenuringThreshold
才能晉升老年代,若是Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於改年齡的對象就能夠直接進入老年代,無需等到MaxPretenuringThreshold
中要求的年齡。
這其實有點相似於負載均衡,輪詢是負載均衡的一種,保證每臺機器都分得一樣的請求。看似很均衡,但每臺機的硬件不通,健康情況不一樣,咱們還能夠基於每臺機接受的請求數,或每臺機的響應時間等,來調整咱們的負載均衡算法。