1. 程式人生 > 實用技巧 >記憶體結構篇:方法區

記憶體結構篇:方法區

一、定義

方法區:與java堆一樣,是各個執行緒共享的記憶體區域。用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料

和堆一樣不需要連續的記憶體,並且可以動態擴充套件,動態擴充套件失敗一樣會丟擲 OutOfMemoryError 異常

該區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,但是一般比較難實現(尤其是型別的解除安裝,條件相當苛刻)。

方法區的演變過程:

JDK8以前,HotSpot 虛擬機器使用永久代來實現方法區,使得垃圾收集器能夠像Java堆一樣管理這部分記憶體。因為永久代有 -XX:MaxPermSize 的上限,這種設計導致 Java 應用更容易遇到記憶體溢位的問題(會丟擲 OutOfMemoryError 異常)。

為了更容易管理方法區,在 JDK6 時,有了逐步改成採用本地記憶體來實現方法區的計劃。到了 JDK7 ,已經把原本放在永久代的字串常量池、靜態變數等移出;而到了 JDK8 ,完成廢棄了永久代的概念,將原來永久代的資料分到了堆和元空間(使用系統記憶體,只要沒到物理最大記憶體就沒有上限,可以手動設定元空間最大記憶體如: -XX MaxMetaspaceSize=8m)中,元空間儲存類的元資訊,靜態變數和常量池等放入堆中。

二、組成

三、方法區記憶體溢位

  • JDK1.8 以前會導致永久代(PermGen space)記憶體溢位
  • JDK1.8 之後會導致元空間(Metaspace)記憶體溢位

可能出現溢位的場景:

  • spring
  • mybatis

四、執行時常量池

  • 常量池,就是一張表,虛擬機器指令根據這張常量表找到要執行的類名、方法名、引數型別、字面量等資訊
  • 執行時常量池,常量池是*.class 檔案中的,當該類被載入,它的常量池資訊就會放入執行時常量池,並把裡面的符號地址變為真實地址
  • 執行時常量池時方法區的一部分
  • 除了在編譯期生成的常量,還允許動態生成,例如 String 類的 intern()

五、StringTable

它的底層資料結構是HashTable

主要存放字串常量

面試題:

// StringTable [ "a", "b" ,"ab" ]  hashtable 結構,不能擴容
public class Demo1_22 {
    // 常量池中的資訊,都會被載入到執行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字串物件
    // ldc #2 會把 a 符號變為 "a" 字串物件
    // ldc #3 會把 b 符號變為 "b" 字串物件
    // ldc #4 會把 ab 符號變為 "ab" 字串物件

    public static void main(String[] args) {
        String s1 = "a"; // 懶惰的
        String s2 = "b";
        String s3 = "ab";   //先從常量池中查詢,如果沒有就建立
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab"),在堆中建立了物件(s1、s2是變數)
        String s5 = "a" + "b";  // javac 在編譯期間的優化,結果已經在編譯期確定為ab(a、b是變數,可以直接從常量池中取出,常量字串拼接)
        String s6 = s4.intern();

        // 問
        System.out.println(s3 == s4);   // false
        System.out.println(s3 == s5);   // true
        System.out.println(s3 == s6);   // true

        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();

        // 問,如果調換了【最後兩行程式碼】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2);   // false;如果調換後,1.8的結果為true,1.6的結果為false,因為1.6中是複製出一個物件放入串池

    }
}

六、StringTable特性

  • 常量池中的字串僅是符號,第一次用到時才變為物件
  • 利用串池的機制,來避免重複建立字串物件
  • 字串變數拼接的原理是 StringBuilder(1.8)
  • 字串常量拼接的原理是編譯器優化
  • 可以使用 intern 方法,主動將串池中還沒有的字串物件放入串池
    • 1.8 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有則放入串池,會把串池中的物件返回
    • 1.6 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有會把此物件複製一份,放入串池,會把串池中的物件返回

七、StringTable位置

  • jdk6:StringTable儲存在永久代
  • jdk8:StringTable儲存在
/**
 * 演示 StringTable 位置
 * 在jdk8下設定 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下設定 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

八、StringTable垃圾回收(在堆中,會被垃圾回收)

測試程式碼

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * -Xmx10m(最大堆記憶體)
 * -XX:+PrintStringTableStatistics(列印字串表的統計資訊,檢視字串的例項個數,佔用的大小資訊)
 * -XX:+PrintGCDetails -verbose:gc(列印垃圾回收的詳細資訊,垃圾回收的次數,時間等)
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 110000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

執行結果:

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->772K(9728K), 0.0008440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2820K->812K(9728K), 0.0008588 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2860K->836K(9728K), 0.0014016 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] 
110000
Heap
....

九、StringTable效能調優

  • 調整 -XX:StringTableSize=桶個數
  • 考慮將字串物件是否入池(使用intern方法,多個相同值只儲存一份)
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
        String line = null;
        long start = System.nanoTime();
        while (true) {
            line = reader.readLine();
            if(line == null) {
                break;
            }
            address.add(line.intern()); //使用intern方法後,line未被引用會被垃圾回收,減少了字串的個數,減少堆記憶體的佔用
        }
        System.out.println("cost:" +(System.nanoTime()-start)/1000000);
    }
}
System.in.read();