天天看點

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

相信大家在處理線上問題的時候,一定遇到過讓人頭疼的OutOfMemoryError異常。當JVM虛拟機記憶體中沒有足夠配置設定記憶體,并且垃圾收集器也無法提供更多的記憶體時就會抛出。

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

對于抛出這個異常資訊,排查起來有時候也比較麻煩,是配置設定的記憶體空間過小、是記憶體中加載的資料量過大、還是類似集合中引用對象過多沒有及時回收、或者是代碼中出現了死循環等等情況。

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

在這篇文章中,我們不讨論怎麼避免上面說的這個異常或者虛拟機怎麼調優,相應的博文網上也有很多,在這裡就不啰嗦了;在這裡隻簡單介紹一下,從JDK1.8之後,虛拟機的記憶體中有一塊區域将抛出OutOfMemoryError異常的機率減小了,在我們以後出現對這個異常進行問題排查的時候,可以減少對這一部分記憶體區域的關注;

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

我開始說廢話了

在讨論這個問題之前,先讓我們看一下下面的一段有意思的Java代碼在同一台電腦的不同的JDK環境下運作,傳回結果的變化,希望可以借此窺探HotSpot虛拟機運作時記憶體的變化:

下面是第一段代碼:

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

傳回結果為true還是false呢?

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

讓我們公布一下答案:

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

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

JDK1.6的運作結果:

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

JDK1.7的運作結果:

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

JDK1.8的運作結果:

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

上面的代碼能說明啥呢,好像和JVM虛拟機記憶體變化沒啥特别大的關系吧?

在回答這個問題之前,讓我們先大緻了解一下JVM虛拟機記憶體模型的劃分,才能明白這段代碼結果反映出的問題:

相信看過周志明先生著的《深入了解Java虛拟機》第二版的童鞋,應該都知道關于虛拟機記憶體區域的劃分:程式計數器、棧記憶體、堆記憶體和方法區

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

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

其中程式計數器是一個“線程私有”的一小塊記憶體空間,同時各線程之間計數器互不影響,獨立存儲,它可以看做是目前線程所執行的位元組碼的行号訓示器。位元組碼解釋器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令;且如果正在執行的是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.6至JDK1.7到JDK1.8—從PermGen到Metaspace

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

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

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

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

同時設定 PermSize 和 MaxPermSize的大小。

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

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

讓我們看一下答案:

JDK1.6的運作結果:

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

JDK1.7的運作結果:

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

JDK1.8的運作結果:

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

在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也被移除了。

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

從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