JVM-String常量池與運行時常量池


Start with JVM

周志明先生著-《深入理解Java虛擬機》,書買回來好幾天了,但是最近才准備開始搞一搞了(哭瞎…..)。首先是第一章的Java以及JVM發展歷史,大概知道了現行的應用最廣泛的Java虛擬機是HotSpot,當然一些商業公司也有使用自己的虛擬機。

JVM運行時數據區

這是放在Java內存區域與內存溢出異常里面的必備知識,描述了Java虛擬機在運行時的數據區域
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
這里寫圖片描述
私有
- 程序計數器:記錄當前線程所執行字節碼的行號指示器
- 虛擬機棧:存放了當前線程調用方法的局部變量表、操作數棧、動態鏈接、方法返回值等信息(可以理解為線程的棧)
- 本地方法棧:為虛擬機使用的Native方法提供服務,后多與JVM Stack合並為一起

共享
- Java堆:占據了虛擬機管理內存中最大的一塊(沒想到吧),唯一目的就是存放對象實例(與引用是兩個概念),也是垃圾回收器主要管理的地方,故又稱GC堆。先開坑,后面講垃圾回收機制再詳述
- 方法區:存儲加載的類信息、常量區、靜態變量、JIT(即時編譯器)處理后的數據等,類的信息包含類的版本、字段、方法、接口等信息。需要注意是常量池就在方法區中,也是我們這次需要關注的地方。

提一下這個Native方法

指得就是Java程序調用了非Java代碼,算是一種引入其它語言程序的接口

看一下方法區

方法區因為總是存放不會輕易改變的內容,故又被稱之為“永久代”。HotSpot也選擇把GC分代收集擴展至方法區,但也容易遇到內存溢出問題。可以選擇不實現垃圾回收,但如果回收就主要涉及常量池的回收和類的卸載(這里開坑,后續補上鏈接)

運行時常量池

回歸本次討論正題,主要是在看Java和C++的一些原理時,老是有“常量池”這個我一知半解的討厭的字詞,煩的一批,今天我就來探一探究竟。

JVM中運行時常量池在方法區中,因為是建立在JDK1.7/1.8的基礎上來研究這個,所以我先認為String常量池在堆中。Class文件中除了類的版本、字段、方法、接口等描述信息,還有常量池,用於存放編譯期生成的各種字面量和符號引用

運行時常量池與Class文件常量池區別

  • JVM對Class文件中每一部分的格式都有嚴格的要求,每一個字節用於存儲那種數據都必須符合規范上的要求才會被虛擬機認可、裝載和執行;但運行時常量池沒有這些限制,除了保存Class文件中描述的符號引用,還會把翻譯出來的直接引用也存儲在運行時常量區
  • 相較於Class文件常量池,運行時常量池更具動態性,在運行期間也可以將新的變量放入常量池中,而不是一定要在編譯時確定的常量才能放入。最主要的運用便是String類的intern()方法
  • 在方法區中,常量池有運行時常量池和Class文件常量池;但其中的內容是否完全不同,暫時還未得知

String.intern()

檢查字符串常量池中是否存在String並返回池里的字符串引用;若池中不存在,則將其加入池中,並返回其引用。
這樣做主要是為了避免在堆中不斷地創建新的字符串對象

那class常量池呢?

具體的等分析到Class文件格式再來填這個坑,先來看常量池中的內容:
這里寫圖片描述
看一下dalao的博客Class文件中常量池詳解

看一看String常量池(的特殊姿勢)吧

在研究這個的時候我也上網看了別人的博客,有的人做出了實驗,我也試一下

實驗一

public class Test{
    public static String a = "a";
    public static void main(){
        String b = "b";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用Java自帶的反編譯工具反編譯一下,編譯后輸入javap -verbose Test.cass

這里寫圖片描述
可以發現兩個靜態String變量都放入了常量池中

實驗二

public class Test2{
    public static String str = "laji" + "MySQL";
    public static void main(){
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

在編譯前先分析一波,按理說,既然是靜態String常量,那么理應出現在常量池(Constant Pool)中,但
這里寫圖片描述

來看看進階版的Test2_2

public class Test2_2{
    public static void main(String[] args){
        String string1 = "laji";  
        String string2 = "MySQL";  
        String string3 = string1+string2;  
        String string4 = string1+"C";   
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這個的結果就更有意思了↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
這里寫圖片描述
總商量個實驗,可以看出

  • 對於直接做+運算的兩個字符串(字面量)常量,並不會放入String常量池中,而是直接把運算后的結果放入常量池中
  • 對於先聲明的字符串字面量常量,會放入常量池,但是若使用字面量的引用進行運算就不會把運算后的結果放入常量池中了
  • 總結一下就是JVM會對String常量的運算進行優化,未聲明的,只放結果;已經聲明的,只放聲明

實驗三

public class Test3{
    public static void main(String[] args){
        String str = "laji";
        String str2 = new String("MySQL");
        String str3 = new String("laji");

        System.out.println(str==str3);// 運行后結果為false
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

結果為:
這里寫圖片描述
這個實驗三包含了很多內容,首先是new一個對象時,明明是在堆中實例化一個對象,怎么會出現常量池中?

  • 這里的"MySQL"並不是字符串常量出現在常量池中的,而是以字面量出現的,實例化操作(new的過程)是在運行時才執行的,編譯時並沒有在堆中生成相應的對象
  • 最后輸出的結果之所以是false,就是因為str指向的”laji”是存放在常量池中的,而str3指向的”laji”是存放在堆中的,==比較的是引用(地址),當然是false

實驗四

主要是為了解釋一下intern()方法的用處

public class Test4{
    public static void main(String[] args){
        String str = "laji";
        String str2 = new String("laji");
        String str3 = null;

        System.out.println(str==str2);// 運行后結果為false
        str3 = str2.intern();
        System.out.println(str==str3);// 運行后結果為true
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

顯然,str3在初始化的時候是從字符串常量池中獲取到的值

String常量池隨JDK的改變

JDK1.7中JVM把String常量區從方法區中移除了;JDK1.8中JVM把String常量池移入了堆中,同時取消了“永久代”,改用元空間代替(Metaspace)

import java.util.ArrayList;

public class TestString {

    public static void main(String[] args) {
         String str = "abc";  
            char[] array = {'a', 'b', 'c'};  
            String str2 = new String(array);  
            //使用intern()將str2字符串內容放入常量池 
            str2 = str2.intern();  
            //這個比較用來說明字符串字面常量和我們使用intern處理后的字符串是在同一個地方 
            System.out.println(str == str2);  
            //那好,下面我們就拼命的intern吧 

            ArrayList<String> list = new ArrayList<String>();  
            for (int i = 0; i < 10000000; i++) {  
                String temp = String.valueOf(i).intern();  
                list.add(temp);  
            }  
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

這個實驗最早是2014年有人實驗過的,ta得出的結論是Exception in thread "main" java.lang.OutOfMemoryError: PermGen space,然而時至今日,我自己按照ta的代碼跑了一遍,並沒有出現上述的錯誤,雖然一段時間內內存資源占用呈上升狀態。猜想:所使用JDK版本不同,對於String常量池存放的位置已經發生了改變;或者是兩者的電腦硬件不同
實驗出處

然后,我又看到了這個新的實驗證明String常量池的位置
JVM參數設置:-Xmx5m -XX:MaxPermSize=5m

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

public class TestString2 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List<String> list = new ArrayList<String>();  
        int i = 0;  
        while(true){  
            list.add(String.valueOf(i++).intern());  
        }  
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

因為JDK版本不同的原因,我無法按照上述的代碼得出原博文相同的結果,這是我自己運行出的結果
這里寫圖片描述

sun官方說明:並行/並發回收器在GC回收時間過長時會拋出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC並且回收了不到2%的堆內存。用來避免內存過小造成應用不能正常工作。

對照着結果以及上面的博客可以得知,這顯然是在堆中的垃圾回收發生了異常所致。在內存滿后,會進行垃圾回收,但又會intern新的字符串到String常量池中,那么就會導致垃圾回收器一直不停的干着沒有意義的活,時間一久,自然報錯。同時原文中所提及的這一句話我覺得需要注意一下:

另外一點值得注意的是,雖然String.intern()的返回值永遠等於字符串常量。但這並不代表在系統的每時每刻,相同的字符串的intern()返回都會是一樣的(雖然在95%以上的情況下,都是相同的)。因為存在這么一種可能:在一次intern()調用之后,該字符串在某一個時刻被回收,之后,再進行一次intern()調用,那么字面量相同的字符串重新被加入常量池,但是引用位置已經不同。

綜上,雖自己沒有太多的明確結果證明,但是我想這已經能夠印證JDK版本變化導致的String常量池位置的改變。

日常summary

這個本來是今天計划打算進行的一部分,結果好像進入牛角尖了,一定要深入一下…..,結果垃圾回收也沒有看多少,明天繼續。
但終於算是把這一塊搞的一清二楚了,


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM