JVM之內存分配和回收機制
前言
本篇主要介紹JVM內存分配和回收策略,內容主要節選自《深入理解java虛擬機》。
一、內存分配策略
1. 堆內存模型
組成:
- 新生代 默認占堆空間的三分之一,由於在新生代對象大多都是朝生夕死,則采用的是復制算法,在復制的期間會有頻繁的Minor GC。
- 老年代 默認占堆空間的三分之二,老年代對象大多都是長期存活的,則采用的是標記算法,老年代的Major GC開銷非常大。
- Eden 新生代分為2個區 Eden和Survivor區 其中Eden區默認占新生代十分之八的空間,對象優先進入Eden區。
- Survivor 當Eden區內存滿瞭之後會進行一次Minor GC存活對象會進入Survivor區,默認占新生代十分之二空間, 其中它又均分為From區和To區,在Survivor區的對象每熬過一次從From區到TO區則年齡+1。
大多情況下,對象在新生代Eden區中分配。當Eden沒有足夠的空間分配對象時虛擬機會發起一次Minor GC。
2.2 大對象直接到老年代
大對象即需要大量連續內存空間的對象(例如很長的字符串及數組)。虛擬機提供瞭一個-XX:PretenureSizeThreshoId參數,令大於這個設置值的對象直接在老年代分配,這樣做的目的是避免在Eden區及兩個區之間發生大量的內存復制。註意PretenureSizeThreshoId參數隻對Serial和ParNew兩款收集器有效。
2.3 動態年齡判斷
為瞭能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到瞭MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進人老年代,無須等到MaxTenuringThreshoId中要求的年齡。
2.4 內存擔保機制
在發生Minor GC之前,虛擬機會先檢查Survivor空間是否夠用,如果夠用則直接進行Minor GC。否則進行檢查老年代最大連續可用空間是否大於新生代的總和,假如大於,那麼這個時候發生Minor GC是安全的。假如不大於,那麼需要判斷HandlePromotionFailure設置是否允許擔保失敗。假如允許,則繼續判定老年代最大可用的連續空間是否大於平均晉升到老年代對象的平均值,如果大於,這個時候可以發生Minor GC ,如果小於或者設置HandlePromotionFailure不允許擔保失敗,則需要做一次Full GC。通常會把HandlePromotionFailure開關打開,以減少Full GC。
2.5 長期存活對象
虛擬機給每個對象定義瞭一個對象年齡(Age)計數器(存在於對象頭中)。如果對象在Eden出生並經過第一次MinorGC後仍然存活,並且能被Survivor容納的話,將被移動到Survwor空間中,並且對象年齡設為1。對象在Survivor區中每“熬過”一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshoId設置。
二、對象存活
判斷對象存活一般有兩種方式: 引用計數算法和可達性分析算法。
1.引用計數算法
原理:
引用計數算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數器屬性。用於記錄對象被引用的情況。對於一個對象A,隻要有任何一個對象引用瞭A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。隻要對象A的引用計數器的值為0,即表示對象A不可能再被使用,可進行回收。
優點:
實現簡單,垃圾對象便於辨識;判定效率高,回收沒有延遲性。
缺點:
它需要單獨的字段存儲計數器,這樣的做法增加瞭存儲空間的開銷。每次復制都需要更新計數器,伴隨著加法和減法操作,這增加瞭時間開銷。引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法。 2.可達性分析算法
定義:相對於引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
原理:
可達性分析算法是以根對象集合(GCRoots)為起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)。如果目標對象沒有任何引用鏈相連,則是不可達的,就意味著該對象己經死亡,可以標記為垃圾對象。在可達性分析算法中,隻有能夠被根對象集合直接或者間接連接的對象才是存活對象。
GC ROOT 對象:
虛擬機棧中引用的對象;
比如:各個線程被調用的方法中使用到的參數、局部變量等。本地方法棧內 JNI(通常說的本地方法)引用的對象;方法區中類靜態屬性引用的對象;
比如:Java類的引用類型靜態變量方法區中常量引用的對象;
比如:字符串常量池(string Table)裡的引用所有被同步鎖 synchronized 持有的對象;Java虛擬機內部的引用。
基本數據類型對應的 Class 對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統類加載器。 3.再談引用 強引用:強引用在代碼中普遍存在,例如Object obj=new Object() 這類的引用。隻要強引用存在,垃圾回收器永遠不會回收掉被引用的對象。軟引用:軟引用來描述一些還有用但並非必須的對象,在系統要發生內存溢出之前,將會把這些對象列入回收范圍之中進行第二次回收,如果第二次回收還沒有足夠的內存,才會拋出內存溢出異常。弱引用:弱引用也是用來描述必須對象的,但是它的強度比軟引用更弱,被弱引用關聯的對象隻能生存到下一次垃圾回收發生之前。當垃圾回收器工作時,無論內存是否足夠,都回收掉隻被弱引用關聯的對象。虛引用:它是最弱的一種引用關系。它無法通過虛引用來取得一個對象實例。唯一目的就是能在這個對象被回收之前會收到一個系統通知。
三、內存回收
1.堆內存回收
是JVM所管理內存最大的一塊,也是gc回收的主要區域。
1.1 哪些對象能回收?
堆內存中對象存活是使用可達性分析算法來判斷,其中非存活對象由GC回收掉。這個就是虛擬機需要回收堆的對象。
1.2 如何回收?
Minor GC:新生代收集,目標隻是新生代的垃圾收集;
Major GC:老年代收集,目標是老年代的垃圾收集(具體說隻有CMS會有單獨收集老年代的行為);
Full GC:收集整個java堆和方法區的垃圾收集。這裡補充說明一下雖然網上很多說什麼Full GC就是Major GC,在這裡我要重申一下並不是,具體看書上描述如下:
Mixed GC:收集整個新生代以及部分老年代的垃圾收集,僅G1支持。(類似於Full GC)
1.3 什麼時候回收?
Minor GC觸發條件:
Eden區域滿瞭,會觸發Minor GC;新生對象需要分配到新生代的Eden,當Eden區的內存不夠時需要進行MinorGC。
Major GC觸發條件:
老年代區域設置的閾值空間滿瞭,會觸發Major GC;新對象需要分配到老年代,此時老年代設置閾值可用空間不足時觸發Major GC。
Full GC觸發條件:
內存擔保機制 ,Survivor空間不足時,判斷是否允許擔保失敗,如果不允許則進行Full GC。如果允許,並且每次晉升到老年代的對象平均大小>老年代最大可用連續內存空間,也會進行Full GC;MinorGC後存活的對象超過瞭老年代剩餘空間;方法區內存不足時;程序中調用瞭System.gc()方法,可用通過-XX:+ DisableExplicitGC來禁止調用System.gc;CMS GC異常,CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,會觸發Full GC。 2.方法區回收
方法區主要回收廢棄的常量池和不再使用類型,但這個2類對象存活的判斷還不一樣。
2.1 常量池
同堆的對象存活類似-可達性分析法,具體請參考之前的可達性分析法。
2.1 類型數據 該類索引的實例都已經被回收;加載該類的類加載器已經被回收;該類對應的java.lang.class對象沒有任何地方被引用。
以上都是我簡單總結,以下是書上關於方法區回收描述的內容:
其實從書上就可以看出來,關於方法區OOM問題大都是在程序中是有大量使用反射、動態代理、CGLIB等框架,如果在實際開發中遇到關於可以從以上幾個維度來定位問題。
總結
本篇所有理論知識都是摘抄於《深入理解java虛擬機》,有部分是自己簡單總結,JVM內存分配和回收是我們在分析JVM調優和相關問題的基石,建議看完我本篇的去多看幾遍《深入理解java虛擬機》。