新四季網

java中的常量池在哪個區(徹底弄懂java中的常量池)

2023-05-18 08:01:00 1

JVM有幾種常量池

主要分為: Class文件常量池、運行時常量池,全局字符串常量池,以及基本類型包裝類對象常量池。

Class文件常量池

class文件是一組以字節為單位的二進位數據流,在java代碼的編譯期間,我們編寫的java文件就被編譯為.class文件格式的二進位數據存放在磁碟中,其中就包括class文件常量池。 class文件中存在常量池(非運行時常量池),其在編譯階段就已經確定,jvm規範對class文件結構有著嚴格的規範,必須符合此規範的class文件才能被jvm任何和裝載。為了方便說明,我們寫個簡單的類

class JavaBean{ private int value = 1; public String s = "abc"; public final static int f = 0x101; public void setValue(int v){ final int temp = 3; this.value = temp v; } public int getValue{ return value; }}

通過javac命令編譯之後,用javap -v 命令查看編譯後的文件:

class JavaBasicKnowledge.JavaBean minor version: 0 major version: 52 flags: ACC_SUPERConstant pool: #1 = Methodref #6.#29 // java/lang/Object."":V #2 = Fieldref #5.#30 // JavaBasicKnowledge/JavaBean.value:I #3 = String #31 // abc #4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String; #5 = Class #33 // JavaBasicKnowledge/JavaBean #6 = Class #34 // java/lang/Object #7 = Utf8 value #8 = Utf8 I #9 = Utf8 s #10 = Utf8 Ljava/lang/String; #11 = Utf8 f #12 = Utf8 ConstantValue #13 = Integer 257 #14 = Utf8 #15 = Utf8 V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 LocalVariableTable #19 = Utf8 this #20 = Utf8 LJavaBasicKnowledge/JavaBean; #21 = Utf8 setValue #22 = Utf8 (I)V #23 = Utf8 v #24 = Utf8 temp #25 = Utf8 getValue #26 = Utf8 I #27 = Utf8 SourceFile #28 = Utf8 StringConstantPool.java #29 = NameAndType #14:#15 // "":V #30 = NameAndType #7:#8 // value:I #31 = Utf8 abc #32 = NameAndType #9:#10 // s:Ljava/lang/String; #33 = Utf8 JavaBasicKnowledge/JavaBean #34 = Utf8 java/lang/Object

可以看到這個命令之後我們得到了該class文件的版本號、常量池、已經編譯後的字節碼(這裡未列出)。既然是常量池,那麼其中存放的肯定是常量,那麼什麼是「常量」呢? class文件常量池主要存放兩大常量:字面量和符號引用。

1) 字面量: 字面量接近java語言層面的常量概念,主要包括:

文本字符串,也就是我們經常申明的: public String s = "abc";中的"abc"

#9 = Utf8 s #3 = String #31 // abc #31 = Utf8 abc

用final修飾的成員變量,包括靜態變量、實例變量和局部變量

#11 = Utf8 f #12 = Utf8 ConstantValue #13 = Integer 257

這裡需要說明的一點,上面說的存在於常量池的字面量,指的是數據的值,也就是abc和0x101(257),通過上面對常量池的觀察可知這兩個字面量是確實存在於常量池的。

image.png

這張圖是我們理解的jvm運行時數據區的結構,但是還有不完整的地方,

image.png

這張圖中,可以看到,方法區實際上是在一塊叫「非堆」的區域包含——可以簡單粗略的理解為非堆中包含了永生代,而永生代中又包含了方法區和字符串常量池

image.png

其中的Interned String就是全局共享的「字符串常量池(String Pool)」,和運行時常量池不是一個概念。但我們在代碼中申明String s1 = "Hello";這句代碼後,在類加載的過程中,類的class文件的信息會被解析到內存的方法區裡。

class文件裡常量池裡大部分數據會被加載到「運行時常量池」,包括String的字面量;但同時「Hello」字符串的一個引用會被存到同樣在「非堆」區域的「字符串常量池」中,而"Hello"本體還是和所有對象一樣,創建在Java堆中。

當主線程開始創建s1時,虛擬機會先去字符串池中找是否有equals(「Hello」)的String,如果相等就把在字符串池中「Hello」的引用複製給s1;如果找不到相等的字符串,就會在堆中新建一個對象,同時把引用駐留在字符串池,再把引用賦給str。

當用字面量賦值的方法創建字符串時,無論創建多少次,只要字符串的值相同,它們所指向的都是堆中的同一個對象。

字符串常量池的本質

字符串常量池是JVM所維護的一個字符串實例的引用表,在HotSpot VM中,它是一個叫做StringTable的全局表。在字符串常量池中維護的是字符串實例的引用,底層C 實現就是一個Hashtable。這些被維護的引用所指的字符串實例,被稱作」被駐留的字符串」或」interned string」或通常所說的」進入了字符串常量池的字符串」。

強調一下:運行時常量池在方法區(Non-heap),而JDK1.7後,字符串常量池被移到了heap區,因此兩者根本就不是一個概念

String"字面量" 是何時進入字符串常量池的?

先說結論: 在執行ldc指令時,該指令表示int、float或String型常量從常量池推送至棧頂

JVM規範裡Class文件的常量池項的類型,有兩種東西:

CONSTANT_Utf8_infoCONSTANT_String_info

在HotSpot VM中,運行時常量池裡,CONSTANT_Utf8_info可以表示Class文件的方法、欄位等等,其結構如下:

image.png

首先是1個字節的tag,表示這是一個CONSTANT_Utf8_info結構的常量,然後是兩個字節的length,表示要儲存字節的長度,之後是一個字節的byte數組,表示真正的儲存的length個長度的字符串。這裡需要注意的是,一個字節只是代表這裡有一個byte類型的數組,而這個數組的長度當然可以遠遠大於一個字節。當然,由於CONSTANT_Utf8_info結構只能用u2即兩個字節來表示長度,因此長度的最大值為2byte,也就是65535

CONSTANT_String_info是String常量的類型,但它並不直接持有String常量的內容,而是只持有一個index,這個index所指定的另一個常量池項必須是一個CONSTANT_Utf8類型的常量,這裡才真正持有字符串的內容

image.png

CONSTANT_Utf8會在類加載的過程中就全部創建出來,而CONSTANT_String則是lazy resolve的,在第一次引用該項的ldc指令被第一次執行到的時候才會resolve。在尚未resolve的時候,HotSpot VM把它的類型叫做JVM_CONSTANT_UnresolvedString,內容跟Class文件裡一樣只是一個index;等到resolve過後這個項的常量類型就會變成最終的JVM_CONSTANT_String

也就是說,就HotSpot VM的實現來說,加載類的時候,那些字符串字面量會進入到當前類的運行時常量池,不會進入全局的字符串常量池(即在StringTable中並沒有相應的引用,在堆中也沒有對應的對象產生),在執行ldc指令時,觸發lazy resolution這個動作

ldc字節碼在這裡的執行語義是:到當前類的運行時常量池(runtime constant pool,HotSpot VM裡是ConstantPool ConstantPoolCache)去查找該index對應的項,如果該項尚未resolve則resolve之,並返回resolve後的內容。

在遇到String類型常量時,resolve的過程如果發現StringTable已經有了內容匹配的java.lang.String的引用,則直接返回這個引用,反之,如果StringTable裡尚未有內容匹配的String實例的引用,則會在Java堆裡創建一個對應內容的String對象,然後在StringTable記錄下這個引用,並返回這個引用出去

可見,ldc指令是否需要創建新的String實例,全看在第一次執行這一條ldc指令時,StringTable是否已經記錄了一個對應內容的String的引用。

String.intern用法

String.intern官方給的定義:

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

實際上,就是去拿String的內容去Stringtable裡查表,如果存在,則返回引用,不存在,就把該對象的"引用"存在Stringtable表裡。

public class RuntimeConstantPoolOOM{ public static void main(String[] args) { String str1 = new StringBuilder("計算機").append("軟體").toString; System.out.println(str1.intern == str1); String str2 = new StringBuilder("ja").append("va").toString; System.out.println(str2.intern == str2); }}

以上代碼,在 JDK6 下執行結果為 false、false,在 JDK7 以上執行結果為 true、false。

首先我們調用StringBuilder創建了一個"計算機軟體"String對象,因為調用了new關鍵字,因此是在運行時創建,之前JVM中是沒有這個字符串的。

在 JDK6 下,intern會把首次遇到的字符串實例複製到永久代中,返回的也是這個永久代中字符串實例的引用;而在JDK1.7開始,intern方法不再複製字符串實例,String 的 intern 方法首先將嘗試在常量池中查找該對象的引用,如果找到則直接返回該對象在常量池中的引用地址

因此在1.7中,「計算機軟體」這個字符串實例只存在一份,存在於java堆中!通過3中的分析,我們知道當String str1 = new StringBuilder("計算機").append("軟體").toString;這句代碼執行完之後,已經在堆中創建了一個字符串對象,並且在全局字符串常量池中保留了這個字符串的引用,那麼str1.intern直接返回這個引用,這當然滿足str1.intern == str1——都是他自己嘛;對於引用str2,因為JVM中已經有「java」這個字符串了,因此new StringBuilder("ja").append("va").toString會重新創建一個新的「java」字符串對象,而intern會返回首次遇到的常量的實例引用,因此他返回的是系統中的那個"java"字符串對象引用(首次),因此會返回false

在 JDK6 下 str1、str2 指向的是新創建的對象,該對象將在 Java Heap 中創建,所以 str1、str2 指向的是 Java Heap 中的內存地址;調用 intern 方法後將嘗試在常量池中查找該對象,沒找到後將其放入常量池並返回,所以此時 str1/str2.intern 指向的是常量池中的地址,JDK6常量池在永久代,與堆隔離,所以 s1.intern和s1 的地址當然不同了。

public class Test2 { public static void main(String[] args) { /** * 首先設置 持久代最大和最小內存佔用(限定為10M) * VM args: -XX:PermSize=10M -XX:MaxPremSize=10M */ List list = new ArrayList; // 無限循環 使用 list 對其引用保證 不被GC intern 方法保證其加入到常量池中 int i = 0; while (true) { // 此處永久執行,最多就是將整個 int 範圍轉化成字符串並放入常量池 list.add(String.valueOf(i ).intern); } }}

以上代碼在 JDK6 下會出現 Perm 內存溢出,JDK7 or high 則沒問題。

JDK6 常量池存在持久代,設置了持久代大小後,不斷while循環必將撐滿 Perm 導致內存溢出;JDK7 常量池被移動到 Native Heap(Java Heap,HotSpot VM中不區分native堆和Java堆),所以即使設置了持久代大小,也不會對常量池產生影響;不斷while循環在當前的代碼中,所有int的字符串相加還不至於撐滿 Heap 區,所以不會出現異常。

JAVA 基本類型的封裝類及對應常量池

java中基本類型的包裝類的大部分都實現了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數類型的包裝類則沒有實現。另外上面這5種整型的包裝類也只是在對應值小於等於127時才可使用對象池,也即對象不負責創建和管理大於127的這些類的對象。

public class StringConstantPool{ public static void main(String[] args){ //5種整形的包裝類Byte,Short,Integer,Long,Character的對象, //在值小於127時可以使用常量池 Integer i1=127; Integer i2=127; System.out.println(i1==i2);//輸出true //值大於127時,不會從常量池中取對象 Integer i3=128; Integer i4=128; System.out.println(i3==i4);//輸出false //Boolean類也實現了常量池技術 Boolean bool1=true; Boolean bool2=true; System.out.println(bool1==bool2);//輸出true //浮點類型的包裝類沒有實現常量池技術 Double d1=1.0; Double d2=1.0; System.out.println(d1==d2); //輸出false }}

在JDK5.0之前是不允許直接將基本數據類型的數據直接賦值給其對應地包裝類的,如:Integer i = 5; 但是在JDK5.0中支持這種寫法,因為編譯器會自動將上面的代碼轉換成如下代碼:Integer i=Integer.valueOf(5);這就是Java的裝箱.JDK5.0也提供了自動拆箱:Integer i =5; int j = i;

以上就是我的分享,感謝各位大佬們耐心看完文章,覺得有所收穫的朋友們可以點個關注轉發收藏一下哦,感謝支持!

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了