從JDK1.6至JDK1.7到JDK1.8—從PermGen到Metaspace

相信大家在處理線上問題的時候,一定遇到過讓人頭疼的OutOfMemoryError異常。當JVM虛擬機內存中沒有足夠分配內存,並且垃圾收集器也無法提供更多的內存時就會拋出。

對於拋出這個異常信息,排查起來有時候也比較麻煩,是分配的內存空間過小、是內存中加載的數據量過大、還是類似集合中引用對象過多沒有及時回收、或者是代碼中出現了死循環等等情況。

在這篇文章中,我們不討論怎麼避免上面說的這個異常或者虛擬機怎麼調優,相應的博文網上也有很多,在這裏就不囉嗦了;在這裏只簡單介紹一下,從JDK1.8之後,虛擬機的內存中有一塊區域將拋出OutOfMemoryError異常的概率減小了,在我們以後出現對這個異常進行問題排查的時候,可以減少對這一部分內存區域的關注;

以下文章中若出現不嚴謹或者錯誤的地方,也歡迎大家指正。

我開始說廢話了

在討論這個問題之前,先讓我們看一下下面的一段有意思的Java代碼在同一臺電腦的不同的JDK環境下運行,返回結果的變化,希望可以藉此窺探HotSpot虛擬機運行時內存的變化:

下面是第一段代碼:

返回結果爲true還是false呢?

讓我們公佈一下答案:

答案是可能是true也有可能是false;假如是在JDK1.8或者JDk1.7的環境下運行,答案是true。但是要是在JDK1.6環境下,返回結果爲false。

下面是小編自己本地測試結果的截圖:

JDK1.6的運行結果:

JDK1.7的運行結果:

JDK1.8的運行結果:

上面的代碼能說明啥呢,好像和JVM虛擬機內存變化沒啥特別大的關係吧?

在回答這個問題之前,讓我們先大致瞭解一下JVM虛擬機內存模型的劃分,才能明白這段代碼結果反映出的問題:

相信看過周志明先生著的《深入理解Java虛擬機》第二版的童鞋,應該都知道關於虛擬機內存區域的劃分:程序計數器、棧內存、堆內存和方法區

總結起來就是下面這幅圖:

其中程序計數器是一個“線程私有”的一小塊內存空間,同時各線程之間計數器互不影響,獨立存儲,它可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令;且如果正在執行的是Native方法,這個計數器值爲空。

在HotSpot虛擬機中,Java虛擬機棧和本地方法棧合二爲一,就是我們常說的棧內存。Java虛擬機棧描述的是Java方法執行的內存模型,它也是線程私有的,本地方法棧是爲使用到的Native方法服務的。

Java虛擬機棧中,每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

Java堆內存是線程共享的一塊內存區域,一般是Java虛擬機所管理的內存中最大的一塊,此內存區域唯一的目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。同時,Java堆內存是垃圾回收器管理的主要區域,從內存回收的角度,由於現在的收集器基本都採用分代收集算法,所以Java堆內存還可以分爲:新生代和老年代,再細一點的有Eden空間、From Survivor空間、To Survivor空間等。關於堆內存的更多細節以及垃圾回收算法等內容,就不在這裏贅述了,感興趣的童鞋可以找相應的文章瞭解、學習。

方法區在HotSpot虛擬機也被稱爲永久代,也是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常亮、靜態變量、即時編譯器變異後的代碼等數據。

介紹完這些,下面才讓我們真正揭曉問題的答案:

相信大家也知道,字符串常量一般放在常量池(Constant Pool)中,但是在JDK1.6環境下,常量池放在永久代(PermGen)中,在執行str2.intern()之前,String str2 = new String("test") + new String("01");通過生成了多個對象,str2最終指向Java堆內存中的“test01”的引用地址。 在執行str2.intern()時,因爲常量池中沒有“test01”這個字符串,會在常量池中生成該字符串的拷貝,將此字符串常量添加到常量池中。在進行String str1 = “test01”字面量賦值的時候,常量池中已經存在該字符串常量,就直接返回了該字符串常量在永久代中的引用地址,因此當調用str2==str1的時候,用Java堆內存中的引用地址和永久代中的引用地址進行比較,一定返回false。

那JDK1.7和JDK1.8的返回結果爲true,是不是說他們倆str2和str1指向的是同一個內存的引用地址呢?答案確實是這樣。

從JDK1.6到JDK1.7,HotSpot虛擬機,關於永久代中的內存分配模型發生了變化,其中一部分就體現在永久代中常量池的變化,JDK1.7之後將字符串常量池從永久代(PermGen)中移動到Java堆內存中了。

因此在JDK1.7和JDK1.8環境下,當調用str2==str1的時候,str1和str2都指向Java堆內存中的同一個字符串的引用地址,因此結果爲true。

通過上面的例子, JDk1.7和JDK1.8相比JDk1.6,常量池由永久代被移動到了Java堆內存中,但是JDK1.7和JDK1.8好像沒什麼變化嘛?

這個問題,讓我們通過下面的第二個例子來進行分析比較。

同樣也是相同的第二段Java代碼:

同時設置 PermSize 和 MaxPermSize的大小。

相信大家也已經看到了,這個是死循環,但是這段代碼的報錯結果會是什麼呢?

讓我們看一下答案:

JDK1.6的運行結果:

JDK1.7的運行結果:

JDK1.8的運行結果:

在JDK1.6環境下,拋出OutOfMemoryError:PermGen space,永久代空間不足。

在JDK1.7和JDK1.8環境下,拋出OutOfMemoryError:Java heap space,堆空間不足。

通過上面的報錯信息也正好印證了咱們上面說的將常量池由永久代移動到了Java堆內存中。但是通過比對JDK1.7和JDk1.8的報錯信息咱們也可以看到,相比於JDK1.7,上圖中JDK1.8的報錯信息中多出了一部分紅色的警告信息。Ignoring option PermSize/MaxPermSize= XXM;support was removerd in 8.0;意思就是,忽略這兩個參數,這兩個參數已經被刪除了。

這是因爲從JDK1.8之後,永久代(PermGen)被完全的移除了,所以永久代的參數-XX:PermSize和-XX:MaxPermSize也被移除了。

從PermGen到Metaspace

回到文章的主題,說了這麼多,其實也只是想說明,相比於HotSpot虛擬機的其他內存區域,虛擬機中方法區的內存區域已經變天啦!

對於JDK1.8, HotSpots取消了永久代,那麼是不是也就沒有方法區了呢?當然不是,方法區是一個規範,規範沒變,它就一直在,只不過取代永久代的是元空間(Metaspace)而已。

在原來的永久代劃分中,永久代用來存放類的元數據信息、靜態常量以及常量池等。現在類的元信息存儲在元空間中,靜態變量和常量池等併入堆中,相當於原來的永久代中的數據,被元空間和堆內存給瓜分了。

相比於之前的永久代劃分,Oracle爲什麼要做這樣的改進呢?

在原來的永久代劃分中,每當一個類初次被加載的時候,它的元數據都會放到永久代中。但是永久代的內存空間也是有大小限制的,如果加載的類太多,很有可能導致永久代內存溢出;同時,永久代大小也不容易確定,因爲這其中有很多影響因素,比如類的總數,常量池的大小和方法數量等,但是PermSize指定太小又很容易造成永久代內存溢出;同時,HotSpot虛擬機的每種類型的垃圾回收器都需要特殊處理永久代中的元數據。永久代會爲GC帶來不必要的複雜度,並且回收效率偏低。將元數據從永久代剝離出來,不僅實現了對元空間的無縫管理,還可以簡化Full GC以及對以後的併發隔離類元數據等方面進行優化。

❶移除永久代的影響

由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。因此,我們就不會遇到永久代存在時的內存溢出錯誤,也不會出現泄漏的數據移到交換區這樣的事情。最終用戶可以爲元空間設置一個可用空間最大值,如果不進行設置,JVM會自動根據類的元數據大小動態增加元空間的容量。但是,永久代的移除並不代表自定義的類加載器泄露問題就解決了。

❷Metaspace內存管理

在元空間中,類和其元數據的生命週期和其對應的類加載器是相同的。元空間的內存管理由元空間虛擬機來完成,每一個類加載器的存儲區域都稱作一個元空間,所有的元空間合在一起就是我們一直說的元空間。

元空間虛擬機負責元空間的分配,其採用的形式爲組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機中存在一個全局的空閒組塊列表。當一個類加載器需要組塊時,它就會從這個全局的組塊列表中獲取並維持一個自己的組塊列表。當一個類加載器不再存活,那麼其持有的組塊將會被釋放,並返回給全局組塊列表。類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式),組塊分配自內存映射區域。這些全局的虛擬內存映射區域以鏈表形式連接,一旦某個虛擬內存映射區域清空,這部分內存就會返回給操作系統。由於類信息並不是固定大小,因此有可能分配的空閒區塊和類需要的區塊大小不同,這種情況下可能導致碎片存在。元空間虛擬機目前並不支持壓縮操作,所以碎片化是目前最大的問題。

❸Metaspace 垃圾回收

先前,對於類的元數據我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機的C++代碼即可完成。只要類加載器存活,其加載的類的元數據也是存活的,就不會被回收掉,對於僵死的類及類加載器的垃圾回收將在元數據使用達到“MaxMetaspaceSize”參數的設定值時進行,但是不會單獨回收某個類,會把相關的空間整個回收掉。在元空間的回收過程中沒有重定位和壓縮等操作。但是元空間內的元數據會進行掃描來確定Java引用。

適時地監控和調整元空間對於減小垃圾回收頻率和減少延時是很有必要的。持續的元空間垃圾回收說明,可能存在類、類加載器導致的內存泄漏或是大小設置不合適。

通過上面,是不是對元空間有一個大概的瞭解呢,當再遇到OutOfMemoryError異常的時候,是不是就可以減少對方法區這部分內存區域查找原因呢?

原文地址:https://www.sohu.com/a/252099792_575744

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章