【JVM】堆體系結構及其記憶體調優

xd會飛的貓發表於2020-05-30

堆體系結構

一個JVM例項只存在一個堆記憶體,堆記憶體的大小是可調節的。類載入器讀取類檔案後,需要把類、方法、常量、變數放在堆記憶體中,儲存所有引用型別的真實資訊,以方便執行器指向,堆記憶體分為三個部分:年輕代、老年代、永久代。

Java7之前,堆記憶體在邏輯上分為:年輕代、老年代、永久代。物理上分為:年輕代、老年代

 Java8:永久代 ---> 元空間

 

新生區是類的誕生、成長、消亡的區域。一個類在新生區產生,最後被垃圾回收器收集。新生區分為伊甸區和倖存者區。倖存者區分為倖存0區,倖存1區。

當伊甸區空間用完的時候,程式還需要建立物件,JVM的垃圾回收器將對伊甸區進行垃圾回收(Minor GC),將伊甸區中不再被其他物件引用的物件進行銷燬,將剩餘的物件移動到倖存0區。

若倖存0區(from區)滿了,對倖存0區進行垃圾回收,將剩餘的物件移動到倖存1區。如果倖存1區(to區)滿了,再移動到養老區。

如果養老區滿了,就產生了Major GC(Full GC),進行養老區的記憶體清理。如果執行了Full GC後依然無法進行物件的儲存,就會產生OOM異常,OutOfMemoryError。

異常:java.lang.OutOfMemoryError: Java heap space

JVM堆記憶體不夠,原因:

  • JVM的堆記憶體設定的太小,可以調整-Xms、-Xmx
  • 程式碼中建立了大量的大物件,並且長時間不能被垃圾回收器收集(存在被引用)

Minor GC的過程

Java堆從GC的角度可以細分為新生代(Eden區、from 存活區、to 存活區,空間比例8:1:1)和老年代(空間比例1:2)。

複製 ☞ 清空 ☞ 互換

1. eden、survivor from 複製到 survivor to,物件年齡+1。

當eden區滿,觸發第一次GC,存活物件拷貝到survivor from區。當eden區再次觸發GC,會掃描eden和from,對這兩個區進行垃圾回收,將存活的物件,複製到to區,物件年齡+1。(如果有物件年齡達到了老年的標準,拷貝到老年代,物件年齡+1)

2. 清空eden、survivor from

清空eden和survivor from中物件,此時from為。

3. survivor from 和 survivor to 互換

to區存在物件,變成下一次GC的from區,from區成為下一次GC的to區,部分物件會在form和to區域複製往來15次(JVM的MaxTenuringshold引數預設是15),如果最終還是存活,就存入老年代。

方法區和永久代

參考自部落格:https://www.jianshu.com/p/66e4e64ff278

在JDK1.6及之前,執行時常量池是方法區的一個部分,同時方法區裡面儲存了類的後設資料資訊、靜態變數、即時編譯器編譯後的程式碼(比如spring 使用IOC或者AOP建立bean時,或者使用cglib,反射的形式動態生成class資訊等)等。在JDK1.7及以後,JVM已經將執行時常量池從方法區中移了出來,在JVM堆開闢了一塊區域存放常量池。

方法區和堆都是各個執行緒共享的記憶體區域,方法區用於儲存虛擬機器載入的類資訊、普通常量、靜態常量、編譯器編譯後的程式碼等,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它還有一個別名叫Non-Heap,目的是和堆分開。

方法區常被成為永久代,嚴格來說二者不同,只是用永久代來實現方法區而已,方法區和永久代的關係很像Java中介面和類的關係,類實現了介面,而永久代就是HotSpot虛擬機器對虛擬機器規範中方法區的一種實現方式。

永久代在JDK1.7之前有,是一個常駐記憶體區域,用於存放JDK自身攜帶的class、interface的後設資料,也就是說它儲存的是執行環境必須的類資訊,被裝在進此區域的資料是不會被垃圾回收器回收掉的,關閉jvm才會釋放這個區域所佔的記憶體。

HotSpot虛擬機器中存在三種垃圾回收現象,minor GC、major GC和full GC。對新生代進行垃圾回收叫做minor GC,對老年代進行垃圾回收叫做major GC,同時對新生代、老年代和永久代進行垃圾回收叫做full GC。許多major GC是由minor GC觸發的,所以很難將這兩種垃圾回收區分開。major GC和full GC通常是等價的,收集整個GC堆。但因為HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的full GC還是major GC。

元空間

參考自部落格:https://www.jianshu.com/p/66e4e64ff278

HotSpot虛擬機器在1.8之後已經取消了永久代,改為元空間,類的元資訊被儲存在元空間中。元空間沒有使用堆記憶體,而是與堆不相連的本地記憶體區域。所以,理論上系統可以使用的記憶體有多大,元空間就有多大,所以不會出現永久代存在時的記憶體溢位問題。
 
這項改造也是有必要的,永久代的調優是很困難的,雖然可以設定永久代的大小,但是很難確定一個合適的大小,因為其中的影響因素很多,比如類數量的多少、常量數量的多少等。永久代中的後設資料的位置也會隨著一次full GC發生移動,比較消耗虛擬機器效能。同時,HotSpot虛擬機器的每種型別的垃圾回收器都需要特殊處理永久代中的後設資料。將後設資料從永久代剝離出來,不僅實現了對元空間的無縫管理,還可以簡化Full GC以及對以後的併發隔離類後設資料等方面進行優化。

堆記憶體調優

在JDK1.7中

在JDK1.8中,元空間取代永久代。元空間和永久代的最大的區別是永久代使用的是JVM的堆記憶體,元空間不在虛擬機器中,而是使用本機實體記憶體。預設清空下,元空間只受本地記憶體限制,類的後設資料放入本地記憶體,字串常量池和型別靜態變數放入java堆,類的後設資料的載入量不再受MaxPermSize控制,而是由系統實際的可用空間來控制。

-Xms:初始分配大小,預設為實體記憶體的1/64

-Xmx:最大分配記憶體,預設為實體記憶體的1/4

-XX:+PrintGCDetails:輸出詳細的GC處理日誌

配置完Xms、Xmx後的輸出結果

java.lang.OutOfMemoryError: Java heap space異常GC處理日誌:

[GC (Allocation Failure) [PSYoungGen: 2045K->488K(2560K)] 2045K->781K(9728K), 0.0014360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2534K->488K(2560K)] 2827K->1548K(9728K), 0.0008101 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2171K->504K(2560K)] 4318K->3194K(9728K), 0.0006870 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2207K->0K(2560K)] [ParOldGen: 7037K->2826K(7168K)] 9245K->2826K(9728K), [Metaspace: 3454K->3454K(1056768K)], 0.0051352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5000K->5000K(9728K), 0.0003304 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5000K->5000K(9728K), 0.0002962 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5000K->3913K(7168K)] 5000K->3913K(9728K), [Metaspace: 3455K->3455K(1056768K)], 0.0028924 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3913K->3913K(8704K), 0.0005099 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3913K->3889K(7168K)] 3913K->3889K(8704K), [Metaspace: 3455K->3455K(1056768K)], 0.0072665 secs] [Times: user=0.06 sys=0.02, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
    at java.lang.StringBuilder.append(StringBuilder.java:208)
    at day05JVM01.T2.main(T2.java:15)

YoungGC

[GC (Allocation Failure)  記憶體分配失敗

[PSYoungGen: 2045K->488K(2560K)] 2045K->781K(9728K), 0.0014360 secs] 

[GC型別:GC前young區的記憶體佔用->GC後young區的記憶體佔用(新生代的總記憶體)] GC前JVM堆記憶體佔用->GC後JVM堆記憶體佔用(JVM堆的總記憶體),GC耗時

[Times: user=0.00 sys=0.00, real=0.00 secs] 

[GC使用者耗時,系統耗時,實際耗時]

 FullGC

[Full GC (Allocation Failure)

[PSYoungGen: 0K->0K(1536K)]

[ParOldGen: 3913K->3889K(7168K)] 3913K->3889K(8704K),

[Metaspace: 3455K->3455K(1056768K)], 0.0072665 secs]

[Times: user=0.06 sys=0.02, real=0.01 secs]

什麼是GC?

GC是分類收集演算法,JVM在進行GC的時候並不是每次對三個區域一起回收,大部分時候是回收新生代。頻繁收集Young區,較少收集Old區,基本不動元空間。GC按照回收的區域分成了:普通GC minor GC和全域性GC Full GC

Minor GC:只針對新生代區域的GC,發生在新生代的垃圾收集,因為大多數JAVA物件存活率都不高,所以Minor GC的操作非常頻繁,垃圾回收的速度比較快。

Full GC:指發生在老年代的垃圾收集操作,出現Full GC,經常會伴隨至少一次的Minor GC(但不絕對)。Full GC的速度一般比Minor GC 慢10倍以上。

GC有四大演算法:引用計數法、複製演算法、標記清除、標記壓縮。

 

 

 

相關文章