第二篇介紹了Java
內存運行時區域,其中程序計數器、虛擬機棧、本地方法棧 三個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的,所以這幾個區域的內存分配和回收都具有肯定性。在這幾個區域內不須要過多考慮回收的問題,由於方法結束或線程結束時,內存天然就跟隨着回收了。算法
而Java
堆 和 方法區 則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同。咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器 所關注的是這部份內存。編程
如何判斷Java
中一個對象應該 「存活」 仍是 「死去」,這是 垃圾回收器要作的第一件事。後端
Java
堆 中每一個具體對象(不是引用)都有一個引用計數器。當一個對象被建立並初始化賦值後,該變量計數設置爲1
。每當有一個地方引用它時,計數器值就加1。當引用失效時,即一個對象的某個引用超過了生命週期(出做用域後)或者被設置爲一個新值時,計數器值就減1。任何引用計數爲0
的對象能夠被看成垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。緩存
優勢:多線程
引用計數收集器執行簡單,斷定效率高,交織在程序運行中。對程序不被長時間打斷的實時環境比較有利。架構
缺點:框架
難以檢測出對象之間的循環引用。同時,引用計數器增長了程序執行的開銷。因此Java語言並無選擇這種算法進行垃圾回收。異步
可達性分析算法也叫根搜索算法,經過一系列的稱爲 GC Roots
的對象做爲起點,而後向下搜索。搜索所走過的路徑稱爲引用鏈 (Reference Chain
), 當一個對象到 GC Roots
沒有任何引用鏈相連時, 即該對象不可達,也就說明此對象是 不可用的。分佈式
以下圖所示: Object5
、Object6
、Object7
雖然互有關聯, 但它們到GC Roots
是不可達的, 所以也會被斷定爲可回收的對象。微服務
GC根對象
在Java
中, 可做爲GC Roots
的對象包括如下四種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
本地方法棧 中 JNI
(Native
方法)引用的變量
方法區 中類靜態屬性引用的變量
方法區 中常量引用的變量
JVM中用到的全部現代GC算法在回收前都會先找出全部仍存活的對象。可達性分析算法是從離散數學中的圖論引入的,程序把全部的引用關係看做一張圖。下圖展現的JVM中的內存佈局能夠用來很好地闡釋這一律念:
在代碼中廣泛存在的,相似Object obj = new Object()
這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
有用但並不是必需 的對象,可用SoftReference
類來實現軟引用。在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。
非必需 的對象,但它的強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前,JDK
提供了WeakReference
類來實現弱引用。不管當前內存是否足夠,用軟引用相關聯的對象都會被回收掉。
虛引用也稱爲幽靈引用或幻影引用,是最弱的一種引用關係,JDK
提供了PhantomReference
類來實現虛引用。爲一個對象設置虛引用的惟一目的是:能在這個對象在垃圾回收器回收時收到一個系統通知。
一個對象是否應該在垃圾回收器在GC
時回收,至少要經歷兩次標記過程。
第一次標記過程,經過可達性分析算法分析對象是否與GC Roots
可達。通過第一次標記,而且被篩選爲不可達的對象會進行第二次標記。
第二次標記過程,判斷不可達對象是否有必要執行finalize
方法。執行條件是當前對象的finalize
方法被重寫,而且還未被系統調用過。若是容許執行那麼這個對象將會被放到一個叫F-Query
的隊列中,等待被執行。
注意:因爲
finalize
由一個優先級比較低的Finalizer
線程運行,因此該對象的的finalize
方法不必定被執行,即便被執行了,也不保證finalize
方法必定會執行完。若是對象第二次小規模標記,即finalize
方法中拯救本身,只須要從新和引用鏈上的任一對象創建關聯便可。
本節具體介紹一下各類垃圾回收算法的思想:
標記-清除算法對根集合進行掃描,對存活的對象進行標記。標記完成後,再對整個空間內未被標記的對象掃描,進行回收。
優勢:
實現簡單,不須要進行對象進行移動。
缺點:
標記、清除過程效率低,產生大量不連續的內存碎片,提升了垃圾回收的頻率。
這種收集算法解決了標記清除算法存在的效率問題。它將內存區域劃分紅相同的兩個內存塊。每次僅使用一半的空間,JVM
生成的新對象放在一半空間中。當一半空間用完時進行GC
,把可到達對象複製到另外一半空間,而後把使用過的內存空間一次清理掉。
優勢:
按順序分配內存便可,實現簡單、運行高效,不用考慮內存碎片。
缺點:
可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。
標記-整理算法 採用和 標記-清除算法 同樣的方式進行對象的標記,但後續不直接對可回收對象進行清理,而是將全部的存活對象往一端空閒空間移動,而後清理掉端邊界之外的內存空間。
優勢:
解決了標記-清理算法存在的內存碎片問題。
缺點:
仍須要進行局部對象移動,必定程度上下降了效率。
當前商業虛擬機都採用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活週期將內存劃分爲幾塊。通常包括年輕代、老年代 和 永久代,如圖所示:
絕大多數最新被建立的對象會被分配到這裏,因爲大部分對象在建立後會很快變得不可達,因此不少對象被建立在新生代,而後消失。對象從這個區域消失的過程咱們稱之爲 minor GC
。
新生代 中存在一個Eden
區和兩個Survivor
區。新對象會首先分配在Eden
中(若是新對象過大,會直接分配在老年代中)。在GC
中,Eden
中的對象會被移動到Survivor
中,直至對象知足必定的年紀(定義爲熬過GC
的次數),會被移動到老年代。
能夠設置新生代和老年代的相對大小。這種方式的優勢是新生代大小會隨着整個堆大小動態擴展。參數 -XX:NewRatio
設置老年代與新生代的比例。例如 -XX:NewRatio=8
指定 老年代/新生代 爲8/1
. 老年代 佔堆大小的 7/8
,新生代 佔堆大小的 1/8
(默認便是 1/8
)。
例如:
-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
複製代碼
對象沒有變得不可達,而且重新生代中存活下來,會被拷貝到這裏。其所佔用的空間要比新生代多。也正因爲其相對較大的空間,發生在老年代上的GC
要比新生代要少得多。對象從老年代中消失的過程,能夠稱之爲major GC
(或者full GC
)。
像一些類的層級信息,方法數據 和方法信息(如字節碼,棧 和 變量大小),運行時常量池(JDK7
以後移出永久代),已肯定的符號引用和虛方法表等等。它們幾乎都是靜態的而且不多被卸載和回收,在JDK8
以前的HotSpot
虛擬機中,類的這些**「永久的」** 數據存放在一個叫作永久代的區域。
永久代一段連續的內存空間,咱們在JVM
啓動以前能夠經過設置-XX:MaxPermSize
的值來控制永久代的大小。可是JDK8
以後取消了永久代,這些元數據被移到了一個與堆不相連的稱爲元空間 (Metaspace
) 的本地內存區域。
JDK8
堆內存通常是劃分爲年輕代和老年代,不一樣年代 根據自身特性採用不一樣的垃圾收集算法。
對於新生代,每次GC
時都有大量的對象死亡,只有少許對象存活。考慮到複製成本低,適合採用複製算法。所以有了From Survivor
和To Survivor
區域。
對於老年代,由於對象存活率高,沒有額外的內存空間對它進行擔保。於是適合採用標記-清理算法和標記-整理算法進行回收。
周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
歡迎關注技術公衆號:零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。