JVM8-一道字符串題帶你認(rèn)識(shí)常量池

JVM8-一道字符串題題帶你認(rèn)識(shí)常量池

請(qǐng)先做題并把答案記在心里

package sandwich.test2;

/**
 * @author sandwich
 * @date 2021/4/20
 */
public class StringTest {
    public static void main(String[] args) {
        String a = "abc";
        String b = new String("abc");
        String c = b.intern();
        System.out.println(a == b);
        System.out.println(b == c);
        System.out.println(a == c);
    }
}

答案在文章最后面

常量池

常量池的數(shù)據(jù)存儲(chǔ)在方法區(qū),分以下三種

1.Class常量池(靜態(tài)常量池)

在 class 文件中除了有類(lèi)的版本、字段、方法和接口等描述信息外,還有一項(xiàng)信息是常量池 (Constant Pool Table),用于存放編譯期間生成的各種字面量符號(hào)引用。
如下是一個(gè)class反匯編的內(nèi)容,請(qǐng)看Constant pool部分內(nèi)容

PS D:\git\test\target\classes\sandwich> javap -v .\Person.class
Classfile /D:/git/test/target/classes/sandwich/Person.class
  Last modified 2021年4月17日; size 957 bytes
  MD5 checksum df5c1bcb5d0f1d5be2998dd1d8d6b44e
  Compiled from "Person.java"
public class sandwich.Person
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // sandwich/Person
  super_class: #14                        // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #14.#35        // java/lang/Object."<init>":()V
   #2 = Class              #36            // sandwich/Person
   #3 = Methodref          #2.#35         // sandwich/Person."<init>":()V
   #4 = Methodref          #2.#37         // sandwich/Person.work:()I
   #5 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Class              #40            // java/lang/StringBuilder
   #7 = Methodref          #6.#35         // java/lang/StringBuilder."<init>":()V
   #8 = String             #41            // result=
   #9 = Methodref          #6.#42         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Methodref          #6.#43         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #11 = Methodref          #6.#44         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #13 = Methodref          #14.#47        // java/lang/Object.hashCode:()I
  #14 = Class              #48            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lsandwich/Person;
  #22 = Utf8               work
  #23 = Utf8               ()I
  #24 = Utf8               i
  #25 = Utf8               I
  #26 = Utf8               j
  #27 = Utf8               main
  #28 = Utf8               ([Ljava/lang/String;)V
  #29 = Utf8               args
  #30 = Utf8               [Ljava/lang/String;
  #31 = Utf8               person
  #32 = Utf8               z
  #33 = Utf8               SourceFile
  #34 = Utf8               Person.java
  #35 = NameAndType        #15:#16        // "<init>":()V
  #36 = Utf8               sandwich/Person
  #37 = NameAndType        #22:#23        // work:()I
  #38 = Class              #49            // java/lang/System
  #39 = NameAndType        #50:#51        // out:Ljava/io/PrintStream;
  #40 = Utf8               java/lang/StringBuilder
  #41 = Utf8               result=
  #42 = NameAndType        #52:#53        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #43 = NameAndType        #52:#54        // append:(I)Ljava/lang/StringBuilder;
  #44 = NameAndType        #55:#56        // toString:()Ljava/lang/String;
  #45 = Class              #57            // java/io/PrintStream
  #46 = NameAndType        #58:#59        // println:(Ljava/lang/String;)V
  #47 = NameAndType        #60:#23        // hashCode:()I
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/System
  #50 = Utf8               out
  #51 = Utf8               Ljava/io/PrintStream;
  #52 = Utf8               append
  #53 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #54 = Utf8               (I)Ljava/lang/StringBuilder;
  #55 = Utf8               toString
  #56 = Utf8               ()Ljava/lang/String;
  #57 = Utf8               java/io/PrintStream
  #58 = Utf8               println
  #59 = Utf8               (Ljava/lang/String;)V
  #60 = Utf8               hashCode
{
  public sandwich.Person();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lsandwich/Person;

  public int work();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_3
         1: istore_1
         2: iconst_5
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: ireturn
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lsandwich/Person;
            2       9     1     i   I
            4       7     2     j   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #2                  // class sandwich/Person
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method work:()I
        12: istore_2
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: new           #6                  // class java/lang/StringBuilder
        19: dup
        20: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        23: ldc           #8                  // String result=
        25: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: iload_2
        29: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        32: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        35: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: aload_1
        39: invokevirtual #13                 // Method java/lang/Object.hashCode:()I
        42: pop
        43: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 13
        line 20: 38
        line 21: 43
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      44     0  args   [Ljava/lang/String;
            8      36     1 person   Lsandwich/Person;
           13      31     2     z   I
}
SourceFile: "Person.java"

字面量:給基本類(lèi)型變量賦值的方式就叫做字面量或者字面值。 比如:String a=“b” ,這里“b”就是字符串字面量,同樣類(lèi)推還有整數(shù)字面值、浮點(diǎn)類(lèi)型字面量、字符字面量。
符號(hào)引用 :符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)。符號(hào)引用可以是任何形式的字面量,JAVA 在編譯的時(shí)候一個(gè)每個(gè) java 類(lèi)都會(huì)被編譯成一個(gè) class 文件,但在編譯的時(shí)候虛擬機(jī)并不知道所引用類(lèi)的地址(實(shí)際地址),就用符號(hào)引用來(lái)代替,而在類(lèi)的解析階段(后續(xù) JVM 類(lèi)加載會(huì)具體講到)就是為了把 這個(gè)符號(hào)引用轉(zhuǎn)化成為真正的地址的階段。 一個(gè) java 類(lèi)(假設(shè)為 People 類(lèi))被編譯成一個(gè) class 文件時(shí),如果 People 類(lèi)引用了 Tool 類(lèi),但是在編譯時(shí) People 類(lèi)并不知道引用類(lèi)的實(shí)際內(nèi)存地址,因 此只能使用符號(hào)引用(org.simple.Tool)來(lái)代替。而在類(lèi)裝載器裝載 People 類(lèi)時(shí),此時(shí)可以通過(guò)虛擬機(jī)獲取 Tool 類(lèi)的實(shí)際內(nèi)存地址,因此便可以既將符號(hào) org.simple.Tool 替換為 Tool 類(lèi)的實(shí)際內(nèi)存地址。

2.運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池(Runtime Constant Pool)是每一個(gè)類(lèi)或接口的常量池(Constant_Pool)的運(yùn)行時(shí)表示形式,它包括了若干種不同的常量: 從編譯期可知的數(shù)值字面量到必須運(yùn)行期解析后才能獲得的方法或字段引用。(這個(gè)是虛擬機(jī)規(guī)范中的描述,很生澀) 運(yùn)行時(shí)常量池是在類(lèi)加載完成之后,將 Class 常量池中的符號(hào)引用值轉(zhuǎn)存到運(yùn)行時(shí)常量池中,類(lèi)在解析之后,將符號(hào)引用替換成直接引用。
運(yùn)行時(shí)常量池在 JDK1.7 版本之后,就移到堆內(nèi)存中了,這里指的是物理空間,而邏輯上還是屬于方法區(qū)(方法區(qū)是邏輯分區(qū))。 在 JDK1.8 中,使用元空間代替永久代來(lái)實(shí)現(xiàn)方法區(qū),但是方法區(qū)并沒(méi)有改變,所謂"Your father will always be your father"。變動(dòng)的只是方法 區(qū)中內(nèi)容的物理存放位置,但是運(yùn)行時(shí)常量池和字符串常量池被移動(dòng)到了堆中。但是不論它們物理上如何存放,邏輯上還是屬于方法區(qū)的

3.字符串常量池

字符串常量池這個(gè)概念是最有爭(zhēng)議的。
我們從它的作用和 JVM 設(shè)計(jì)它用于解決什么問(wèn)題的點(diǎn)來(lái)分析它。 以 JDK1.8 為例,字符串常量池是存放在堆中,并且與 java.lang.String 類(lèi)有很大關(guān)系。設(shè)計(jì)這塊內(nèi)存區(qū)域的原因在于:String 對(duì)象作為 Java 語(yǔ)言中重
要的數(shù)據(jù)類(lèi)型,是內(nèi)存中占據(jù)空間最大的一個(gè)對(duì)象。高效地使用字符串,可以提升系統(tǒng)的整體性能。 所以要徹底弄懂,我們的重心其實(shí)在于深入理解 String。

3.1 String 類(lèi)分析(JDK1.8)

String 對(duì)象是對(duì) char 數(shù)組進(jìn)行了封裝實(shí)現(xiàn)的對(duì)象,主要有 2 個(gè)成員變量:char 數(shù)組,hash 值。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

3.2 String 對(duì)象的不可變性

了解了 String 對(duì)象的實(shí)現(xiàn)后,你有沒(méi)有發(fā)現(xiàn)在實(shí)現(xiàn)代碼中 String 類(lèi)被 final 關(guān)鍵字修飾了,而且變量 char 數(shù)組也被 final 修飾了。 我們知道類(lèi)被 final 修飾代表該類(lèi)不可繼承,而 char[]被 final+private 修飾,代表了 String 對(duì)象不可被更改。Java 實(shí)現(xiàn)的這個(gè)特性叫作 String 對(duì)象的不 可變性,即 String 對(duì)象一旦創(chuàng)建成功,就不能再對(duì)它進(jìn)行改變。
Java 這樣做的好處在哪里呢?
1.保證 String 對(duì)象的安全性。 假設(shè) String 對(duì)象是可變的,那么 String 對(duì)象將可能被惡意修改。
2.保證 hash 屬性值不會(huì)頻繁變更。確保了唯一性,使得類(lèi)似 HashMap 容器才能實(shí)現(xiàn)相應(yīng)的 key-value 緩存功能.
3.可以實(shí)現(xiàn)字符串常量池。在 Java 中,通常有兩種創(chuàng)建字符串對(duì)象的方式,一種是通過(guò)字符串常量的方式創(chuàng)建,如 String str=“abc”;另一種是字 符串變量通過(guò) new 形式的創(chuàng)建,如 String str = new String(“abc”)。

3.3 String 的創(chuàng)建方式及內(nèi)存分配的方式

1、String str=“abc”; 當(dāng)代碼中使用這種方式創(chuàng)建字符串對(duì)象時(shí),JVM 首先會(huì)檢查該對(duì)象是否在字符串常量池中,如果在,就返回該對(duì)象引用,否則新的字符串將在常量池中 被創(chuàng)建。這種方式可以減少同一個(gè)值的字符串對(duì)象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。(str 只是一個(gè)引用)

String str = "abc"
代碼編譯加載時(shí),會(huì)在常量池中創(chuàng)建常量"abc", 運(yùn)行時(shí),返回常量池中的字符串引用

字符串常量池
“abc”

2、String str = new String(“abc”) 。 首先在編譯類(lèi)文件時(shí),"abc"常量字符串將會(huì)放入到常量結(jié)構(gòu)中,在類(lèi)加載時(shí),“abc"將會(huì)在常量池中創(chuàng)建;其次,在調(diào)用 new 時(shí),JVM 命令將會(huì)調(diào)用 String 的構(gòu)造函數(shù),同時(shí)引用常量池中的"abc” 字符串,在堆內(nèi)存中創(chuàng)建一個(gè) String 對(duì)象;最后,str 將引用 String 對(duì)象。

String str = new String("abc")
1.代碼編譯加載時(shí),會(huì)在常量池中創(chuàng)建常量“abc”

字符串常量池
“abc”

2.在調(diào)用new是,會(huì)在堆中創(chuàng)建String對(duì)象,并引用常量池中的字符串對(duì)象char[]數(shù)組,并返回String對(duì)象引用。

字符串常量池
String對(duì)象 “abc”

3、 使用 new,對(duì)象會(huì)創(chuàng)建在堆中,同時(shí)賦值的話,會(huì)在常量池中創(chuàng)建一個(gè)字符串對(duì)象,復(fù)制到堆中。 具體的復(fù)制過(guò)程是先將常量池中的字符串壓入棧中,在使用 String 的構(gòu)造方法是,會(huì)拿到棧中的字符串作為構(gòu)方法的參數(shù)。 這個(gè)構(gòu)造函數(shù)是一個(gè) char 數(shù)組的賦值過(guò)程,而不是 new 出來(lái)的,所以是引用了常量池中的字符串對(duì)象。存在引用關(guān)系。

public class Location {
private String city;
private String region;
}

public class model{
    Location location = new Location;
    location.setCity("廣州");
    location.setRegion("珠江新城");
}

在運(yùn)行時(shí),創(chuàng)建的String在堆中,直接創(chuàng)建,不會(huì)在常量池中創(chuàng)建

字符串常量池
廣州
珠江新城

4.String str="ab"+"cd"+"ef"
前面我講過(guò) String 對(duì)象是不可變的,如果我們使用 String 對(duì)象相加,拼接我們想要的字符串,是不是就會(huì)產(chǎn)生多個(gè)
對(duì)象呢?例如以下代碼: 分析代碼可知:首先會(huì)生成 ab 對(duì)象,再生成 abcd 對(duì)象,最后生成 abcdef 對(duì)象,從理論上來(lái)說(shuō),這段代碼是低效的。 編譯器自動(dòng)優(yōu)化了這行代碼,編譯后的代碼,你會(huì)發(fā)現(xiàn)編譯器自動(dòng)優(yōu)化了這行代碼,如下 String str= "abcdef";
5.intern
String 的 intern 方法,如果常量池中有相同值,就會(huì)重復(fù)使用該對(duì)象,返回對(duì)象引用。

/**
 * @author sandwich
 * @date 2021/4/20
 */
public class InternTest {
    public static void main(String[] args) {
        String a = new String("Sandwich").intern();
        String b = new String("Sandwich").intern();
        System.out.println(a == b);
    }
}
//結(jié)果是true

1、new Sting() 會(huì)在堆內(nèi)存中創(chuàng)建一個(gè) a 的 String 對(duì)象,"Sandwich"將會(huì)在常量池中創(chuàng)建
2、在調(diào)用 intern 方法之后,會(huì)去常量池中查找是否有等于該字符串對(duì)象的引用,有就返回引用。
3、調(diào)用 new Sting() 會(huì)在堆內(nèi)存中創(chuàng)建一個(gè) b 的 String 對(duì)象。
4、在調(diào)用 intern 方法之后,會(huì)去常量池中查找是否有等于該字符串對(duì)象的引用,有就返回引用。 所以 a 和 b 引用的是同一個(gè)對(duì)象。


現(xiàn)在可以公布文章開(kāi)頭的答案了
這道題主要是考考常量池
答案是:

false
false
true

題解:a 是常量池的字符串(在方法區(qū))的引用,b在調(diào)用new時(shí)會(huì)在堆中創(chuàng)建String對(duì)象,并引用常量池中的字符串對(duì)象char[]數(shù)組。并返回String對(duì)象引用,c直接返回b在常量池創(chuàng)建的常量引用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容