1. 程式人生 > >JVM(二)Java8記憶體劃分

JVM(二)Java8記憶體劃分

一、永久代

在說java8記憶體模型之前先說一下永久代的概念。

在Java虛擬機器(JVM)內部,class檔案中包括類的版本、欄位、方法、介面等描述資訊,還有執行時常量池,用於存放編譯器生成的各種字面量和符號引用。
在過去類大多是”static”的,很少被解除安裝或收集,因此被稱為“永久的(Permanent)”。同時,由於類class是JVM實現的一部分,並不是由應用建立的,所以又被認為是“非堆(non-heap)”記憶體。

在JDK8之前的HotSpot JVM,存放這些”永久的”的區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在JVM啟動之前通過在命令列設定引數-XX:MaxPermSize來設定永久代最大可分配的記憶體空間,預設大小是64M(64位JVM由於指標膨脹,預設是85M)。永久代的垃圾收集是和老年代(old generation)捆綁在一起的,因此無論誰滿了,都會觸發永久代和老年代的垃圾收集。不過,一個明顯的問題是,當JVM載入的類資訊容量超過了引數-XX:MaxPermSize設定的值時,應用將會報OOM的錯誤

二、jdk1.7與jdk1.8記憶體變化

那jdk1.7 與jdk1.8記憶體比有什麼變化呢?先看一段程式碼在說結論。

package demo.com.test.jvm;

import java.util.ArrayList;
import java.util.List;

public class PermTest {
    // -XX:PermSize=4m -XX:MaxPermSize=4m -Xmx=8m 執行時改一下jvm引數
    static String base = "StringPool";
    public static void main(String[] args) {
        List<String> list = new
ArrayList<String>(); for (int i = 0; i < Integer.MAX_VALUE; i++) { String str = base + base; base = str; list.add(str.intern()); } } }

此段程式碼在jdk1.7上執行時 報如下錯誤

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at demo.com.test.jvm.PermTest.main(PermTest.java:11)

此段程式碼在jdk1.8上執行時 報如下錯誤

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at demo.com.test.jvm.PermTest.main(PermTest.java:11)

我們發現竟然沒有報 java.lang.OutOfMemoryError :PermGen space
那麼可以大致得出 JDK 1.7 和 1.8 將字串常量池由永久代轉移到堆中,並且 JDK 1.8 中已經不存在永久代的結論。

接下來我們在jdk1.8下繼續執行一段程式碼。

package demo.com.test.jvm;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class PermTest2 {
    public static void main(String[] args) {
        // -XX:PermSize=4m -XX:MaxPermSize=4m -Xmx=8m 執行時改一下jvm引數
        URL url = null;
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
        try {
            url = new File("C:\\Users\\lenovo\\Desktop\\blok").toURI().toURL();
            URL[] urls = { url };
            while (true) {
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                loader.loadClass("demo.com.test.jvm.PermTest2");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

本例中使用的 JDK 版本是 1.8,指定的 PermGen 區的大小為 4M。通過每次生成不同URLClassLoader物件來載入PermTest2類,從而生成不同的類物件,執行一段時間發現沒有報”java.lang.OutOfMemoryError: PermGen space ” 異常。同時發現本地的記憶體直線上升。如下圖
本地記憶體使用情況

此時我們也可以得出一個結論在jdk1.8中沒有永久代的概念,並且存放類相關資訊的地方也不在heap(堆)中。那這部分在那裡呢 ? 在元空間裡。

三、Metaspace(元空間)

3.1 metaspace的組成

metaspace其實由兩大部分組成
● Klass Metaspace
● NoKlass Metaspace

Klass Metaspace就是用來存klass的,klass是我們熟知的class檔案在jvm裡的執行時資料結構,不過有點要提的是我們看到的類似A.class其實是存在heap裡的,是java.lang.Class的一個物件例項。這塊記憶體是緊接著Heap的,和我們之前的perm一樣,這塊記憶體大小可通過-XX:CompressedClassSpaceSize引數來控制,這個引數前面提到了預設是1G,但是這塊記憶體也可以沒有,假如沒有開啟壓縮指標就不會有這塊記憶體,這種情況下klass都會存在NoKlass Metaspace裡,另外如果我們把-Xmx設定大於32G的話,其實也是沒有這塊記憶體的,因為會這麼大記憶體會關閉壓縮指標開關。還有就是這塊記憶體最多隻會存在一塊。

NoKlass Metaspace專門來存klass相關的其他的內容,比如method,constantPool(常量池)等,這塊記憶體是由多塊記憶體組合起來的,所以可以認為是不連續的記憶體塊組成的。這塊記憶體是必須的,雖然叫做NoKlass Metaspace,但是也其實可以存klass的內容,上面已經提到了對應場景。

Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以類載入器們要分配記憶體,但是每個類載入器都有一個SpaceManager,來管理屬於這個類載入的記憶體小塊。如果Klass Metaspace用完了,那就會OOM了,不過一般情況下不會,NoKlass Mestaspace是由一塊塊記憶體慢慢組合起來的,在沒有達到限制條件的情況下,會不斷加長這條鏈,讓它可以持續工作。

3.2 metaspace主要相關引數

  • UseLargePagesInMetaspace
  • InitialBootClassLoaderMetaspaceSize
  • MetaspaceSize
  • MaxMetaspaceSize
  • CompressedClassSpaceSize
  • MinMetaspaceExpansion
  • MaxMetaspaceExpansion
  • MinMetaspaceFreeRatio
  • MaxMetaspaceFreeRatio

下面介紹一下常用的幾個

  • MetaspaceSize

預設20.8M左右(x86下開啟c2模式),主要是控制metaspaceGC發生的初始閾值,也是最小閾值。

  • MaxMetaspaceSize

預設基本是無窮大,但是我還是建議大家設定這個引數,因為很可能會因為沒有限制而導致metaspace被無止境使用(一般是記憶體洩漏)而被OS Kill。這個引數會限制metaspace被committed的記憶體大小,會保證committed的記憶體不會超過這個值,一旦超過就會觸發GC,這裡要注意和MaxPermSize的區別,MaxMetaspaceSize並不會在jvm啟動的時候分配一塊這麼大的記憶體出來,而MaxPermSize是會分配一塊這麼大的記憶體的。

  • MinMetaspaceFreeRatio

MinMetaspaceFreeRatio和下面的MaxMetaspaceFreeRatio,主要是影響觸發metaspaceGC的閾值預設40

  • MaxMetaspaceFreeRatio

預設70,這個引數和上面的引數基本是相反的,是為了避免觸發metaspaceGC的閾值過大,而想對這個值進行縮小。

3.3 Metaspace的記憶體分配與管理

Metaspace VM利用記憶體管理技術來管理Metaspace。這使得由不同的垃圾收集器來處理類元資料的工作,現在僅僅由Metaspace VM在Metaspace中通過C++來進行管理。Metaspace背後的一個思想是,類和它的元資料的生命週期是和它的類載入器的生命週期一致的。也就是說,只要類的類載入器是存活的,在Metaspace中的類元資料也是存活的,不能被釋放。

每個類載入器儲存區叫做“a metaspace”。這些metaspaces一起總體稱為”the Metaspace”。僅僅當類載入器不在存活,被垃圾收集器宣告死亡後,該類載入器對應的metaspace空間才可以回收。Metaspace空間沒有遷移和壓縮。但是元資料會被掃描是否存在Java引用。

Metaspace VM使用一個塊分配器(chunking allocator)來管理Metaspace空間的記憶體分配。塊的大小依賴於類載入器的型別。其中有一個全域性的可使用的塊列表(a global free list of chunks)。當類載入器需要一個塊的時候,類載入器從全域性塊列表中取出一個塊,新增到它自己維護的塊列表中。當類載入器死亡,它的塊將會被釋放,歸還給全域性的塊列表。塊(chunk)會進一步被劃分成blocks,每個block儲存一個元資料單元(a unit of metadata)。Chunk中Blocks的分配線性的(pointer bump)。這些chunks被分配在記憶體對映空間(memory mapped(mmapped) spaces)之外。在一個全域性的虛擬記憶體對映空間(global virtual mmapped spaces)的連結串列,當任何虛擬空間變為空時,就將該虛擬空間歸還回作業系統。

這裡寫圖片描述

3.4 Metaspace VM記憶體碎片問題

先前提到的,Metaspace VM使用塊分配器(chunking allocator)。chunk的大小取決於類載入器的型別。由於類class並沒有一個固定的尺寸,這就存在這樣一種可能:可分配的chunk的尺寸和需要的chunk的尺寸不相等,這就會導致記憶體碎片。Metaspace VM還沒有使用壓縮技術,所以記憶體碎片是現在的一個主要關注的問題。
這裡寫圖片描述

3.5 引數應用

改變一下jvm引數 -XX:MaxMetaspaceSize=4m -XX:MetaspaceSize=4m
在次執行 PermTest2 類 會發現,程式會報異常,說明引數有效。
D:>java -XX:MaxMetaspaceSize=4m -XX:MetaspaceSize=4m PermTest2
Error occurred during initialization of VM
OutOfMemoryError: Metaspace

四、總結

其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap。
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過引數來指定元空間的大小。

為什麼要將永久代替換成Metaspace?可能的原因有:
  1、字串存在永久代中,容易出現效能問題和記憶體溢位。
  2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位。
  3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  4、Oracle 可能會將HotSpot 與 JRockit 合二為一。

五、java8記憶體模型圖

這裡寫圖片描述

六、應該掌握的知識

1.在 JDK 1.7 和 1.8 中將字串常量池由永久代轉移到堆中

2.存放類相關資訊的地方也不在heap(堆)中。在元空間裡。

3.在jdk1.8中沒有永久代的概念,

4.metaspace其實由兩大部分組成

● Klass Metaspace
存放klass的,klass是我們熟知的class檔案在jvm裡的執行時資料結構,這個空間的預設大小是1G

● NoKlass Metaspace
專門來存klass相關的其他的內容,比如method,constantPool(常量池)等,這塊記憶體是由多塊記憶體組合起來的,所以可以認為是不連續的記憶體塊組成的。這塊記憶體是必須的

● Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以類載入器們要分配記憶體,但是每個類載入器都有一個SpaceManager,來管理屬於這個類載入的記憶體小塊。如果Klass Metaspace用完了,那就會OOM了,不過一般情況下不會,NoKlass Mestaspace是由一塊塊記憶體慢慢組合起來的,在沒有達到限制條件的情況下,會不斷加長這條鏈,讓它可以持續工作。

5.metaspace主要相關引數

-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

-XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。

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

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

6 Metaspace的記憶體分配與管理 都應該清楚

7為什麼要將永久代替換成Metaspace?

  • 1、字串存在永久代中,容易出現效能問題和記憶體溢位。
  • 2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困
    難,太小容易出現永久代溢位,太大則容易導致老年代溢位。
  • 3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  • 4、Oracle 可能會將HotSpot 與 JRockit 合二為一。

七、參考