GC
中文直譯垃圾回收,是一種回收內存空間避免內存泄漏的機制。當 JVM
內存緊張,經過執行 GC
有效回收內存,轉而分配給新對象從而實現內存的再利用。 JVM
GC
機制雖然無需開發主動參與,減輕很多工做量,可是某些狀況下,自動 GC
將會致使系統性能降低,響應變慢,因此這就須要咱們提早了解掌握 GC
機制。當面對這種狀況時,才能從容不迫的解決問題。另外 GC
機制也是 Java
面試高頻考題,瞭解掌握 GC 是一項必備技能。java
學習 GC
,首先咱們解決三個問題:nginx
咱們先來看一段簡單的代碼。web
上面代碼經過將字符串對象轉化成字節數組,而後寫入本地文件。方法一旦開始執行,就將會在分配必定內存給新建的對象,而後將引用告訴了 str
, bytes
變量。等到方法執行完畢,方法內部局部變量緊接將就會被銷燬。可是這樣僅僅銷燬了局部變量,卻沒有帶走內存上這些實際的對象。這類再也不起做用,沒有被引用的對象,將其歸類爲垃圾。面試
在偌大的內存上存活着無數對象, GC
以前須要準確將這些對象標記出來,分爲存活對象與垃圾對象。這個過程一旦少標記,那就只能等待下次 GC
標記,再回收,這樣將會影響 GC 效率。另外決不能錯標記,將正常存活對象標記爲垃圾。一旦回收正常存活的對象,可能就會引發程序各類崩潰。算法
目前有兩種算法能夠用來標記:數組
引用計數法經過在對象頭分配一個字段,用來存儲該對象引用計數。一旦該對象被其餘對象引用,計數加 1。若是這個引用失效,計數減 1。當引用計數值爲 0 時,表明這個對象已再也不被引用,能夠被回收。架構
引用計數法性能
如上圖所示,當 str
引用堆中對象時,計數值增長爲 1。當 str
變爲 null
時,既再也不引用該對象,計數值減 1。此時該對象就能夠被 GC
回收。學習
引用計數法只須要判斷計數值,因此實現比較簡單,這個過程也比較高效。可是存在一個很嚴重的問題,沒法解決對象循環引用問題。google
引用計數法-1
從上圖能夠看到, a
, b
再也不引用堆中對象,致使計數減一。此時兩個對象內部還存在互相引用,計數值不爲 0,此時 GC
沒辦法回收該對象。
這個算法首先須要按照規則查找當前活躍的引用,將其稱爲 GC Roots
。接着將 GC Roots
做爲根節點出發,遍歷對象引用關係圖,將能夠遍歷(可達)的對象標記爲存活,其他對象當作無用對象。
可達性分析
注意這裏是是 引用 ,而不是對象。
從上圖能夠看到,綠色對象雖然存在循環引用,可是因爲這些對象不能被 GC Roots
遍歷到,因此將會被回收。
能夠被當作 GC Roots
活躍引用包括但不限於如下引用:
還記得剛開始接觸 Java
時,只知道堆棧,對象實例分配在堆中,方法中局部變量位於棧中。實際上 JVM
內存區域劃分更加細緻,分爲:
JVM 運行時內存區域劃分
如圖所示,咱們將內存劃分爲線程私有與線程共享的區域。方法區與堆都是線程共享的區域,這兩部分佔用 JVM
大部份內存,剩下三個小弟將會跟線程綁定,隨着線程消亡,自動將會被 JVM
回收。
堆應該是你們最熟悉的一塊區域,幾乎全部對象實例都將會在此出生,一般也是虛擬機上佔用內存最大一塊區域,簡直就是 JVM
內存中的大哥大。堆內存內部也不是簡簡單單一塊而已,目前將會根據分代算法,將堆分代,不一樣對象位於不一樣區域。這一點咱們下文再詳細瞭解。
方法區將會保存已被虛擬上加載的類信息、常量,靜態變量,字節碼等信息,堆上的對象正式經過方法區這些信息,才能正確建立出來。
虛擬機棧棧由一系列棧幀組成,每一個棧幀其實表明一個方法,棧幀中將會保存一個方法的局部變量表,方法出入口信息,操做棧等。每當調用一個方法,就將會把這個棧幀壓入棧中,執行結束,出棧。
本地方法棧與虛擬機棧比較相似,最大區別在於,虛擬機棧執行的 Java
方法,而本地方法棧將會用來執行 Native
方法服務。下面方法就會在本地方法棧中執行。
<pre class="java" style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important; color: rgb(49, 70, 89); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 300; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
Copy
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
</pre>
程序計算器能夠說是這幾塊區域佔用最小的一部分,可是功能卻十分重要。Java 源代碼經過編譯變成字節碼,而後被 JVM 載入運行以後,將會變成一條條指令,而程序計數器的工做就是告訴當前線程下一條須要執行指令。這樣即便發生了線程切換,等待恢復的時候,當前線程依然知道接下去要執行的指令。
目前主流 GC 算法主要分爲三種:
這是一個最爲基礎也是最容易實現的算法,主要實現步驟分爲兩步:標記,清除。
GC Roots
標記出可達對象。
ps:這個圖着實難畫啊。。。。
能夠看到通過這個算法回收以後,雖然堆空間被清理出來,可是也產生不少 空間碎片 。這就會致使一個新對象根據堆剩餘容量計算,看起來是能夠分配,可是實際分配過程,因爲沒有連續內存,致使虛擬機感知到內存不足,又不得不提早再次觸發 GC
。
可能這裏你就會有疑惑,爲何對象須要分配一塊連續的內存?
這裏引用一下 R 神@RednaxelaFX 答案。
另外這個算法還有一個不足:標記與清除效率比較低。這就竟會致使 GC
佔用時間過長,影響正常程序使用。
爲了解決上述效率問題,誕生複製算法。這個算法將可用內存分爲兩塊,每次只使用其中一塊,當這一塊內存使用完畢,觸發 GC
,將會把存活的對象依次複製到另一塊上,而後再把已使用過的內存一次性清理。
這個算法每次只須要操做一半內存, GC
回收以後也不存在任何空間碎片,新對象內存分配時只須要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。可是這個算法閒置一半內存空間,空間利用效率不高。
PS:複製算法以空間換時間,二者不可兼得
另外對象存活率也會影響複製算法效率。若是對象大部分都是朝生夕死,只須要移動少許存活對象,就能騰出大部分空間。反而若是對象存活率高,這就須要進行較多的複製操做,回收以後也並無多餘內存,這就可能致使頻繁觸發 GC
。
針對這種存活時間長的對象,就須要使用標記-整理算法。
標記-整理算法能夠說是標記-清除算法的改進版,改進了清除致使的空間碎片問題。這個算法分爲兩步:
GC Roots
雖然標記-整理算法解決了標記-清除算法空間碎片問題,也完整利用整個內存空間,可是這個算法問題效率並不高。相較於標記-清除算法,標記-整理算法多增長整理這一步,因此該算法效率還低於標記-清除算法。
從上面三種 GC
算法能夠看到,並無一種空間與時間效率都是比較完美的算法,因此只能作的是綜合利用各類算法特色將其做用到不用的內存區域。
目前商業虛擬機根據對象存活週期不一樣劃份內存區域,通常分爲新生代,老年代。新對象通常狀況都會優先分配在新生代,新生代對象若存活時間大於必定閾值以後,將會移到至老年代。新生代的對象都是短命鬼,老年代的對象都是長壽先生。
新生代每次 GC
以後均可以回收大批量對象,因此比較適合複製算法,只須要付出少許複製存活對象的成本。這裏內存劃分並無按照 1:1 劃分,默認將會按照 8:1:1 劃分紅 Eden
與兩塊 Survivor
空間。每次使用 Eden
與一塊 Survivor
空間,這樣咱們只是閒置 10% 內存空間。不過咱們每次回收並不能保證存活對象小於 10%,在這種狀況下就須要依靠老年代的內存分配擔保。當 Survivor
空間並不能保存剩餘存活對象,就將這些對象經過分配擔保進制移動至老年代。
老年代中對象存活率將會特別高,且沒有額外空間進行分配擔保,因此並不適合複製算法,因此須要使用標記-清除或標記-整理算法。
感謝你看完個人長篇大論,若是以爲對你有幫助的話,能夠動動你敲代碼的小手幫我點個贊。
或者也能夠關注個人公衆號【Java技術zhai】,不按期的技術乾貨內容分享,帶你從新定義架構的魅力!