Java的永久代和元空間及各種常量池

轉載地址:https://www.cnblogs.com/jwcz/p/11772143.html

              https://www.cnblogs.com/shen-qian/p/11277085.html

目錄

元空間替代永久代

永久代

元空間

移除永久代的影響

元空間內存管理

運行時常量池

存在的問題

常量池隨永久代的變化

總結



元空間替代永久代

元空間代替了永久代,所以JVM關於永久代的參數也都作廢了,取而代之的是關於MetaSpace空間的參數。而且Mete Space是屬於直接內存。示意圖:

永久代

Java 的內存中有一塊稱之爲方法區的部分,在 JDK8 之前, Hotspot 虛擬機中的實現方式爲永久代(Permanent Generation),別的JVM都沒有這個東西。

在過去(當自定義類加載器使用不普遍的時候),類幾乎是“靜態的”並且很少被卸載和回收,因此類也可以被看成“永久的”。另外由於類作爲 JVM 實現的一部分,它們不由程序來創建,因爲它們也被認爲是“非堆”的內存。

永久代是一段連續的內存空間,我們在 JVM 啓動之前可以通過設置-XX:MaxPermSize的值來控制永久代的大小,32 位機器默認的永久代的大小爲 64M,64 位的機器則爲 85M。

永久代的垃圾回收和老年代的垃圾回收是綁定的,一旦其中一個區域被佔滿,這兩個區都要進行垃圾回收。但是有一個明顯的問題,由於我們可以通過‑XX:MaxPermSize設置永久代的大小,一旦類的元數據超過了設定的大小,程序就會耗盡內存,並出現內存溢出錯誤 ( java.lang.OutOfMemoryError: PermGen space)。

爲什麼類的元數據佔用內存會那麼大?因爲在 JDK7 之前的 HotSpot 虛擬機中,納入字符串常量池的字符串被存儲在永久代中,因此導致了一系列的性能問題和內存溢出錯誤。

爲了解決這些性能問題,也爲了能夠讓 Hotspot 能和其他的虛擬機一樣管理,元空間就產生了。

元空間

元空間是 Hotspot 在 JDK8 中新加的內容,其本質和永久代類似,都是對 JVM 規範中方法區的實現。不過元空間與永久代之間最大的區別在於:

元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:

-XX:MetaspaceSize 

初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  
-XX:MaxMetaspaceSize
最大空間,默認是沒有限制的。

除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:

-XX:MinMetaspaceFreeRatio

在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集

-XX:MaxMetaspaceFreeRatio

在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集

移除永久代的影響

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

注意:永久代的移除並不代表自定義的類加載器泄露問題就解決了。因此,你還必須監控你的內存消耗情況,因爲一旦發生泄漏,會佔用你的大量本地內存,並且還可能導致交換區交換更加糟糕。

元空間內存管理

元空間的內存管理由元空間虛擬機來完成。

先前,對於類的元數據我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機的 C++ 代碼即可完成。在元空間中,類和其元數據的生命週期和其對應的類加載器是相同的。話句話說,只要類加載器存活,其加載的類的元數據也是存活的,因而不會被回收掉。

準確的來說,每一個類加載器的存儲區域都稱作一個元空間,所有的元空間合在一起就是我們一直說的元空間。當一個類加載器被垃圾回收器標記爲不再存活,其對應的元空間會被回收。在元空間的回收過程中沒有重定位和壓縮等操作。但是元空間內的元數據會進行掃描來確定 Java 引用。

那具體是如何管理的呢?

元空間虛擬機負責元空間的分配,其採用的形式爲組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機中存在一個全局的空閒組塊列表。

  1. 當一個類加載器需要組塊時,它就會從這個全局的組塊列表中獲取並維持一個自己的組塊列表。
  2. 當一個類加載器不再存活時,那麼其持有的組塊將會被釋放,並返回給全局組塊列表。
  3. 類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式)。組塊分配自內存映射區域。這些全局的虛擬內存映射區域以鏈表形式連接,一旦某個虛擬內存映射區域清空,這部分內存就會返回給操作系統。

運行時常量池

運行時常量池在 JDK6 及之前版本的 JVM 中是方法區的一部分,而在 HotSpot 虛擬機中方法區的實現是永久代(Permanent Generation)。所以運行時常量池也是在永久代的。

但是 JDK7 及之後版本的 JVM 已經將字符串常量池從方法區中移了出來,在堆(Heap)中開闢了一塊區域存放字符串常量池。

String.intern()是一個 Native 方法,它的作用是:如果字符串常量池(非運行時常量池)中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此 String 內容相同的字符串,並返回常量池中創建的字符串的引用。

存在的問題

前面已經提到,元空間虛擬機採用了組塊分配的形式,同時區塊的大小由類加載器類型決定。類信息並不是固定大小,因此有可能分配的空閒區塊和類需要的區塊大小不同,這種情況下可能導致碎片存在。元空間虛擬機目前並不支持壓縮操作,所以碎片化是目前最大的問題。

常量池隨永久代的變化

幾種常量池:

(1)靜態常量池:即*.class文件中的常量池,在Class文件結構中,最頭的4個字節存儲魔數,用於確定一個文件是否能被JVM接受,接着4個字節用於存儲版本號,前2個爲次版本號,後2個主版本號,再接着是用於存放常量的常量池,由於常量的數量是不固定的,所以常量池的入口放置一個U2類型的數據(constant_pool_count)存儲常量池容量計數值。

這種常量池佔用class文件絕大部分空間,主要用於存放兩大類常量:字面量和符號引用量,字面量相當於Java語言層面常量的概念,如文本字符串、基礎數據、聲明爲final的常值等;符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:類和接口的全限定名、字段名稱描述符、方法名稱描述符。類的加載過程中的鏈接部分的解析步驟就是把符號引用替換爲直接引用,即把那些描述符(名字)替換爲能直接定位到字段、方法的引用或句柄(地址)。

(2)運行時常量池:虛擬機會將各個class文件中的常量池載入到運行時常量池中,即編譯期間生成的字面量、符號引用,總之就是裝載class文件。爲什麼它叫運行時常量池呢?因爲這個常量池在運行時,裏面的常量是可以增加的。如:“+”連接字符生成新字符後調用 intern()方法(???)、生成基礎數據的包裝類型等等。

(3)字符串常量池 :字符串常量池可以理解爲是分擔了部分運行時常量池的工作。加載時,對於class文件的靜態常量池,如果是字符串就會被裝到字符串常量池中。

(4)整型常量池:Integer,類似字符串常量池。管理-128--127的常量。類似的還有Character、Long等常量池(基本數據類型沒有哦,Double、Float也沒有常量池)

總結就是:

    class文件有常量池存放這個類的信息,佔用了大多數空間。但是運行時所有加載進來的class文件的常量池的東西都要放到運行時常量池,這個運行時常量池還可以在運行時添加常量。字符串常量池、Integer等常量池則是分擔了運行時常量池的工作,

在永久代移除後,字符串常量池也不再放在永久代了,但是也沒有放到新的方法區---元空間裏,而是留在了堆裏(爲了方便回收?)。運行時常量池當然是隨着搬家到了元空間裏,畢竟它是裝類的重要等信息的,有它的地方纔稱得上是方法區。

總結

方法區只是一規範,在不同的虛擬機中的實現是不一樣的,例如永久代和元空間。
用於存放被已經被JVM加載的的類信息,常量,靜態變量,方法數據,構造函數和普通方法等。

java堆物理上有新生代和老年代,邏輯上分爲新生代+老年代+永久代
解析:

永久代其實只是方法區的一個存儲實現,邏輯上劃分爲堆的一部分,爲的是方法區也可以用堆內存的GC垃圾回收機制,而不用重新針對方法區做GC操作,直接使用堆內存的GC就可以了

在java8後,使用元空間替代了永久代,且元空間的內存不在JVM堆中,而是在本地內存中,所以java8元空間,基本不存在OOM內存溢出;

但是java7的永久代位於JVM內存中,存在OOM錯誤;
且在java8中,常量池是位於元空間中,string的實例化放在堆內存中

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