日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

探究Java常量本质及三种常量池(JVM)

發布時間:2025/3/15 java 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 探究Java常量本质及三种常量池(JVM) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

簡介: 探究Java常量本質及三種常量池
可以從他人的博文,還有一些書籍中了解到 常量是放在常量池 中,細節的內容無從得知,相信每個人都會覺得面前的東西是一個幾乎完全的黑盒,總是覺得不舒服,翻閱《深入理解Java虛擬機》,會發現這本書中對常量的介紹更多地偏重于字節碼文件的結構,還有在自動內存管理機制中也介紹了運行時常量池。下面換種思路來看一下

Java中的常量池分為三種形態:靜態常量池,字符串常量池以及運行時常量池。

一、靜態常量池

所謂靜態常量池,即*.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數字)字面量,還包含類、方法的信息,占用class文件絕大部分空間。
這種常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當于Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:

  • 類和接口的全限定名
  • 字段名稱和描述符
  • 方法名稱和描述符
  • 二、運行時常量池

    運行時常量池,則是jvm虛擬機在完成類裝載操作后,將class文件中的常量池載入到內存中,并保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池。

    運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
    String的intern()方法:會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量池。

    那這樣來看,通過靜態常量池,即*.class文件中的常量池 更能夠探究常量的含義了

    下面看一段代碼

    public class Main {public static void main(String[] args) {System.out.println(Father.str);} } class Father{public static String str = "Hello,world";static {System.out.println("Father static block");} }

    輸出結果為

    再看另一個:

    package com.company; public class Main {public static void main(String[] args) {System.out.println(Father.str);} } class Father{public static final String str = "Hello,world";static {System.out.println("Father static block");} }

    結果:
    只有一個

    是不是發現很吃驚啊

    我們對第二個演示的代碼塊進行反編譯一下

    D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class Compiled from "Main.java" public class com.company.Main {public com.company.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return }

    這里有一個Main()是構造方法 下面的是main方法

    0: getstatic # 2 對應的是System.out 3: ldc #4 對應的值 直接是 Hello,world 了 確定的值
    沒有從Father類中取出

    ldc表示將int,float或是String類型的常量值從常量池中推送至棧頂,竟然沒有!!! 即使刪除Father.class文件 這段代碼照樣可以運行 它和Father類 沒有半毛錢的關系了

    實際上,在編譯階段 常量就會被存入到調用這個常量的方法所在的類的常量池當中

    從這個例子中 可以看出這里的str 是一個常量 調用這個常量的方法是main方法 main方法所在的類是Main ,也就是說編譯之后str被放在了該類的常量池中

    本質上,調用類并沒有直接引用到定義常量的類,因此并不會觸發定義常量的類的初始化
    類的初始化 涉及到類的加載機制 這里暫時寫不說 這個留到之后必須要好好說說

    三、字符串常量池(string pool也有叫做string literal pool)

    全局字符串池里的內容是在類加載完成,經過驗證,準備階段之后在堆中生成字符串對象實例,然后將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的。)

    字符串常量池的位置的說法不太準確:

  • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen區(也就是方法區)中;
  • 在JDK7.0版本,字符串常量池被移到了堆中了。
  • 在HotSpot VM里實現的string pool功能的是一個StringTable類,它是一個哈希表,里面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之后就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。
    字符串常量池設計原理
    字符串常量池底層是hotspot的C++實現的,底層類似一個 HashTable(Key-Value), 保存的本質上是字符串對象的引用。看一道比較常見的面試題,下面的代碼創建了多少個 String 對象?

    String s1 = new String("he") + new String("llo"); String s2 = s1.intern(); System.out.println(s1 == s2);
  • 在 JDK 1.6 下輸出是 false,創建了 6 個對象:s1(hello)、new String、he、new
    String、llo、s2(hello);在調用s1.intern()的時候沒有Hello,所以會在堆中創建一個hello,并復制到常量池中;
  • 在 JDK 1.7 及以上的版本輸出是 true,創建了 5 個對象:s1、new String、he、new
    String、llo;在調用s1.intern()的時候沒有Hello,就在常量池中創建一個引用指向堆中的hello中;當然這里沒有考慮GC,但這些對象確實存在或存在過
  • 為什么輸出會有這些變化呢?主要還是字符串池從永久代中脫離、移入堆區的原因, intern() 方法也相應發生了變化:

  • 在 JDK 1.6 中,調用 intern() 首先會在字符串池中尋找 equal()
    相等的字符串,假如字符串存在就返回該字符串在字符串池中的引用;假如字符串不存在,虛擬機會重新在永久代上創建一個實例,將StringTable 的一個表項指向這個新創建的實例。

  • 在 JDK 1.7(及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的對象。字符串存在時和JDK1.6一樣,但是字符串不存在時不再需要重新創建實例,可以直接指向堆上的實例。

  • 由上面兩個圖,也不難理解為什么 JDK 1.6 字符串池溢出會拋出 OutOfMemoryError: PermGen space ,而在JDK 1.7 及以上版本拋出 OutOfMemoryError: Java heap space 。

    四、回到運行常量池(runtime constant pool)

    jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。

    而當類加載到內存中后,jvm就會將靜態常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。

    靜態常量池中存的是字面量和符號引用,也就是說它們存的并不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之后,也就是把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與字符串常量池中所引用的是一致的。

    我們看一個例子

    import java.util.UUID;public class Test {public static void main(String[] args) {System.out.println(TestValue.str);} }class TestValue{public static final String str = UUID.randomUUID().toString();static {System.out.println("TestValue static code");} }

    結果:

    從聲明本身str都是常量,關鍵的是這個常量的值能否在編譯時期確定下來,顯然這里的例子在編譯期的時候顯然是確定不下來的。需要在運行期才能能夠確定下來,這要求目標類要進行初始化

    當常量的值并非編譯期間可以確定的,那么其值不會被放到調用類的常量池中
    這時在程序運行時,會導致主動使用這個常量所在的類,顯然會導致這個類被初始化。
    (這個涉及到類的加載機制,后面會寫這里做個標記)

    反編譯探究一下:

    Compiled from "Test.java" class com.leetcodePractise.tstudy.TestValue {public static final java.lang.String str;com.leetcodePractise.tstudy.TestValue();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnstatic {};Code:0: invokestatic #2 // Method java/util/UUID.randomUUID:()Ljava/util/UUID;3: invokevirtual #3 // Method java/util/UUID.toString:()Ljava/lang/String;6: putstatic #4 // Field str:Ljava/lang/String;9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;12: ldc #6 // String TestValue static code14: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V17: return }

    很明顯TestValue類會初始化出來

    常量介紹完之后 這里記錄一下反編譯及助記符的筆記

    package com.company;public class Main {public static void main(String[] args) {System.out.println(Father.str);System.out.println(Father.s);} }class Father{public static final String str = "Hello,world";public static final short s = 6;static {System.out.println("Father static block");} }

    public class com.company.Main {public com.company.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: return }

    bipush 表示將單字節(-128-127)的常量值推送至棧頂

    再加入

    package com.company;public class Main {public static void main(String[] args) {System.out.println(Father.str);System.out.println(Father.s);System.out.println(Father.t);} }class Father{public static final String str = "Hello,world";public static final short s = 6;public static final int t = 128;static {System.out.println("Father static block");} }

    進行反編譯

    public class com.company.Main {public com.company.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;19: sipush 12822: invokevirtual #6 // Method java/io/PrintStream.println:(I)V25: return }

    sipush表示將一個短整型常量值(-32768~32767)推送至棧頂

    再進行更改

    package com.company;public class Main {public static void main(String[] args) {System.out.println(Father.str);System.out.println(Father.t);} }class Father{public static final String str = "Hello,world";public static final int t = 1;static {System.out.println("Father static block");} } public class com.company.Main {public com.company.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;19: sipush 12822: invokevirtual #6 // Method java/io/PrintStream.println:(I)V25: return }D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class Compiled from "Main.java" public class com.company.Main {public com.company.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: iconst_112: invokevirtual #6 // Method java/io/PrintStream.println:(I)V15: return }

    這里變成了 iconst_1

    iconst 1表示將int類型1推送至棧頂(iconst_m1-iconst_5)

    當大于5的時候 就變為了bipush

    m1對應的是-1

    五、八種基本類型的包裝類和對象池

    java中基本類型的包裝類的大部分都實現了常量池技術(嚴格來說應該叫對象池,在堆上),這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數類型的包裝類則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值小于等于127時才可使用對象池,也即對象不負責創建和管理大于127的這些類的對象。因為一般這種比較小的數用到的概率相對較大。

    public class Test {public static void main(String[] args) {//5種整形的包裝類Byte,Short,Integer,Long,Character的對象,//在值小于127時可以使用對象池Integer i1 = 127; //這種調用底層實際是執行的Integer.valueOf(127),里面用到了IntegerCache對象池Integer i2 = 127;System.out.println(i1 == i2);//輸出true//值大于127時,不會從對象池中取對象Integer i3 = 128;Integer i4 = 128;System.out.println(i3 == i4);//輸出false//用new關鍵詞新生成對象不會使用對象池Integer i5 = new Integer(127);Integer i6 = new Integer(127);System.out.println(i5 == i6);//輸出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} }

    參考文章1
    參考文章2

    總結

    以上是生活随笔為你收集整理的探究Java常量本质及三种常量池(JVM)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。