什麼是GC

GC(Garbage Collection)垃圾收集機制,這也是Java和C/C++之間的主要區別之一。對於一個Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼,對於內存泄漏和溢出的問題,不需要那麼特別注意。

總的來說:GC機制對於JVM中的內存進行標記,確定哪一些內存需要回收。再根據一些回收策略,自動進行回收內存,永不停息的保證JVM中的內存空間。這也就防止了內存泄漏和溢出問題了。

JVM的內存分佈

對於瞭解GC的原理,那麼JVM的內存分佈肯定是需要了解的。在Java運行的時候,JVM管理的內存區域有以下幾塊:

私有內存區:

區域名稱特性程序計數器指示當前程序執行到了哪一行,執行JAVA方法時記錄正在執行的虛擬機字節碼指令地址;執行本地方法時,計數器值爲undefined虛擬機棧用於執行JAVA方法。棧幀存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。程序執行時棧幀入棧;執行完成後棧幀出棧本地方法棧用於執行本地方法,其它和虛擬機棧類似

共享內存區:

區域名稱特性Java堆堆區是理解Java GC機制最重要的區域,沒有之一。JVM管理內存中,最大的一塊。 堆區由所有線程共享,在虛擬機啓動時創建。堆區的存在是爲了存儲對象實例。方法區方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。Java堆的分佈

GC主要針對堆內存,所以將堆內存進行詳細闡述。

堆內存主要分爲三塊:新生代(Youn Generation)、老年代(Old Generation)、持久代(Permanent Generation)。

三代的特點不同,造就了他們使用的GC算法不同:

新生代適合生命週期較短,快速創建和銷燬的對象;老年代適合生命週期較長的對象;持久代在Sun Hotpot虛擬機中就是指方法區(有些JVM根本就沒有持久代這一說法)。

新生代

新生代(Youn Generation):大致分爲Eden區和Survivor區,Survivor區又分爲大小相同的兩部分:FromSpace和ToSpace。新建的對象都是從新生代分配內存,Eden區不足的時候,會把存活的對象轉移到Survivor區。當新生代進行垃圾回收時會出發Minor GC(也稱作Youn GC)。Eden佔比80%,兩塊Survivor佔比20%。

新生代的回收,引用Copy算法。回收過程大致如下:

Eden區第一次內存滿之後,執行MinorGC,清理消亡對象。之後將剩餘存活對象複製到From Survivor區中(此時To區空白,兩個Survivor總有一個空白)第二次Eden區滿之後,再次執行MinorGC,清除Eden中消亡對象。並且將From中的消亡對象清除,將Eden和From Survivor中存活的對象copy到To區。之後Eden再滿,From和To區相互交換。直到To區填滿了,就將所有存活的對象移動到老年代。

注意: 若沒有填滿,每次MinorGC的時候,給存活對象標記+1,根據–Xx:MaxTenuringThreshold(默認15)。標記大於1的時候,同樣移進老年代。

老年代

老年代進行垃圾回收的時候,成爲MajorGC/FullGC。

發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC。

否則,如果小於的話,JVM就查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗)。如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表着如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)

注意: FullGC比MinorGC的速度慢10被以上。因爲FullGC的時候,用戶線程暫停,降低系統性能、吞吐量。

永久代(方法區)

永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。

對於無用的類進行回收,必須保證3點:

類的所有實例都已經被回收加載類的ClassLoader已經被回收類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)

JDK1.8之前,這些數據保存於此。JDK8,將永久代從堆中取出,數據存在本地內存地區(堆外空間)。

回收算法概述

追蹤回收算法(tracing collector)可達性分析算法

通過一系列的稱爲 GC Roots 的對象作爲起點, 然後向下搜索; 搜索所走過的路徑稱爲引用鏈/Reference Chain, 當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的。

如圖:對象5、6、7就是不可達的,需要被回收

按代回收算法(Generational Collector)

當前主流VM垃圾收集都採用”分代收集”(Generational Collection)算法, 這種算法會根據對象存活週期的不同將內存劃分爲幾塊, 如JVM中的 新生代、老年代、永久代. 這樣就可以根據各年代特點分別採用最適當的GC算法:

在新生代: 每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用複製算法, 只需要付出少量存活對象的複製成本就可以完成收集.在老年代: 因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用**“標記—清理”或“標記—整理”**算法來進行回收, 不必進行內存複製, 且直接騰出空閒內存.

注意: 除去按代回收,還有按區回收算法。

分區收集上面介紹的分代收集算法是將對象的生命週期按長短劃分爲兩個部分, 而分區算法則將整個堆空間劃分爲連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的好處是可以控制一次回收多少個小區間.在相同條件下, 堆空間越大, 一次GC耗時就越長, 從而產生的停頓也越長. 爲了更好地控制GC產生的停頓時間, 將一塊大的內存區域分割爲多個小塊, 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減少一次GC所產生的停頓.

複製回收算法(Coping Collector) (新生代)

把堆均分成兩個大小相同的區域,只使用其中的一個區域,直到該區域消耗完。此時垃圾回收器終端程序的執行,通過遍歷把所有活動的對象複製到另一個區域,複製過程中它們是緊挨着佈置的,這樣也可以達到消除內存碎片的目的。複製結束後程序會繼續運行,直到該區域被用完。

但是,這種方法有兩個缺陷:

對於指定大小的堆,需要兩倍大小的內存空間,需要中斷正在執行的程序,降低了執行效率

標記-清理算法 (老年代)

該算法分爲“標記”和“清除”兩個階段: 首先標記出所有需要回收的對象(可達性分析), 在標記完成後統一清理掉所有被標記的對象。

該算法會有以下兩個問題:

1. 效率問題: 標記和清除過程的效率都不高;

2. 空間問題: 標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集.

標記-整理算法 (老年代)

標記清除算法會產生內存碎片問題, 而複製算法需要有額外的內存擔保空間, 於是針對老年代的特點, 又有了標記整理算法. 標記整理算法的標記過程與標記清除算法相同, 但後續步驟不再對可回收對象直接清理, 而是讓所有存活的對象都向一端移動,然後清理掉端邊界以外的內存。

空間分配擔保(Handle Promotion Failure)

在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 由於新生代使用複製收集算法, 爲了提升內存利用率, 只使用了其中一個Survivor作爲輪換備份, 因此當出現大量對象在Minor GC後仍然存活的情況時, 就需要老年代進行分配擔保, 讓Survivor無法容納的對象直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活對象.

但存活對象的大小在實際完成GC前是無法明確知道的。

因此Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間)。

然而取歷次晉升的對象的平均大小也是有一定風險的, 如果某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能導致擔保失敗(Handle Promotion Failure, 老年代也無法存放這些對象了), 此時就只好在失敗後重新發起一次Full GC(讓老年代騰出更多空間).

轉載

此文章是站在各位大佬的肩膀上進行總結的,感謝。

http://www.importnew.com/23035.html

https://blog.csdn.net/antony9118/article/details/51375662

https://blog.csdn.net/anjoyandroid/article/details/78609971

http://baijiahao.baidu.com/s?id=1604308216748480477&wfr=spider&for=pc

相關文章