一、方法區
方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯后的代碼等。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。
在JVM規范中,沒有強制要求方法區必須實現垃圾回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分區域,從而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之后,Hotspot虛擬機便將運行時常量池從永久代移除了。
二、永久代與方法區
涉及到內存模型時,往往會提到永久代,那么它和方法區又是什么關系呢?《Java虛擬機規范》只是規定了有方法區這么個概念和它的作用,並沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。 同時大多數用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集擴展至方法區,或者說使用永久代來實現方法區。因此,我們得到了結論,永久代是HotSpot的概念,方法區是Java虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標准一個是實現。其他的虛擬機實現並沒有永久帶這一說法。在1.7之前在(JDK1.2 ~ JDK6)的實現中,HotSpot 使用永久代實現方法區,HotSpot 使用 GC分代來實現方法區內存回收,可以使用如下參數來調節方法區的大小:
-XX:PermSize
方法區初始大小
-XX:MaxPermSize
方法區最大大小
超過這個值將會拋出OutOfMemoryError異常:java.lang.OutOfMemoryError: PermGen
三、元空間與方法區
對於Java8, HotSpots取消了永久代,那么是不是也就沒有方法區了呢?當然不是,方法區是一個規范,規范沒變,它就一直在。那么取代永久代的就是元空間。它可永久代有什么不同的?存儲位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是連續的,而元空間屬於本地內存;存儲內容不同,元空間存儲類的元信息,靜態變量和常量池等並入堆中。相當於永久代的數據被分到了堆和元空間中。
四、JVM 1.8 永久代—元空間的變動
JDK8 HotSpot JVM 將移除永久區,使用本地內存來存儲類元數據信息並稱之為:元空間(Metaspace)
以下是JVM內存模型中方法區的變動
1.新生代:Eden+From Survivor+To Survivor
2.老年代:OldGen
3.永久代(方法區的實現) : PermGen----->替換為Metaspace(本地內存中)
方法區和“PermGen space”又有着本質的區別。前者是 JVM 的規范,而后者則是 JVM 規范的一種實現,並且只有 HotSpot 才有 “PermGen space”,而對於其他類型的虛擬機,如 JRockit(Oracle)、J9(IBM) 並沒有“PermGen space”。由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導致的垃圾收集
《Java虛擬機規范(JavaSE7)》中也說了方法區是堆的邏輯組成部分。
實際上JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代在JDK1.8才被移除
五、移除永久代的影響
由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。因此,我們就不會遇到永久代存在時的內存溢出錯誤,也不會出現泄漏的數據移到交換區這樣的事情。最終用戶可以為元空間設置一個可用空間最大值,如果不進行設置,JVM會自動根據類的元數據大小動態增加元空間的容量。
注意:永久代的移除並不代表自定義的類加載器泄露問題就解決了。因此,你還必須監控你的內存消耗情況,因為一旦發生泄漏,會占用你的大量本地內存,並且還可能導致交換區交換更加糟糕。
六、元空間內存管理
元空間的內存管理由元空間虛擬機來完成。先前,對於類的元數據我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機的C++代碼即可完成。在元空間中,類和其元數據的生命周期和其對應的類加載器是相同的。話句話說,只要類加載器存活,其加載的類的元數據也是存活的,因而不會被回收掉。
准確的來說,每一個類加載器的存儲區域都稱作一個元空間,所有的元空間合在一起就是我們一直說的元空間。當一個類加載器被垃圾回收器標記為不再存活,其對應的元空間會被回收。在元空間的回收過程中沒有重定位和壓縮等操作。但是元空間內的元數據會進行掃描來確定Java引用。
元空間虛擬機負責元空間的分配,其采用的形式為組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機中存在一個全局的空閑組塊列表。當一個類加載器需要組塊時,它就會從這個全局的組塊列表中獲取並維持一個自己的組塊列表。當一個類加載器不再存活,那么其持有的組塊將會被釋放,並返回給全局組塊列表。類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式)。組塊分配自內存映射區域。這些全局的虛擬內存映射區域以鏈表形式連接,一旦某個虛擬內存映射區域清空,這部分內存就會返回給操作系統。
運行時常量池在JDK1.6及之前版本的JVM中是方法區的一部分,而在HotSpot虛擬機中方法區放在了”永久代(Permanent Generation)”。所以運行時常量池也是在永久代的。
但是JDK1.7及之后版本的JVM已經將字符串常量池從方法區中移了出來,在Java 堆(Heap)中開辟了一塊區域存放字符串常量池。
String.intern()是一個Native方法,它的作用是:如果運行時常量池中已經包含一個等於此String對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此String內容相同的字符串,並返回常量池中創建的字符串的引用。
七、變動原因
字符串存在永久代中,現實使用中易出問題, 由於永久代內存經常不夠用或發生內存泄露,爆出異常 java.lang.OutOfMemoryError: PermGen類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
永久代會為 GC 帶來不必要的復雜度,並且回收效率偏低。
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
即:移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。