String類型在JVM中的內存分配


一、關於常量池

字符串在Java中用的非常得多,Jvm為了減少內存開銷和提高性能,使用字符串常量池來進行優化。

在jdk1.7之前(不包括1.7),Java的常量池是在方法區的地方,方法區是一個運行時JVM管理的內存區域,是一個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態常量等。

運行時常量池是方法區的一部分。

來看一個圖:

(圖片來自https://www.cnblogs.com/ysocean/p/8571426.html)

關於其他的內存分布就不在這介紹了。

 

 

而在jdk1.7和它以后,方法區的常量池被移到了堆中,見圖:(圖片來自https://www.cnblogs.com/ysocean/p/8571426.html)

 

 

 

 

 

二、new String("xxx")和 = "xxx"

在了解常量池后,我們再來看這兩個創建String對象的方法。

 

先來看使用引號""創建字符串的方式

  • 單獨(注意是單獨)使用引號來創建字符串的方式,字符串都是常量,在編譯期已經確定存儲在常量池中了。
  • 用引號創建一個字符串的時候,首先會去常量池中尋找有沒有相等的這個常量對象,沒有的話就在常量池中創建這個常量對象;有的話就直接返回這個常量對象的引用。

所以看這個例子:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);//true

這個例子的結果是true,首先 String str1 = "hello",會先到常量池中檢查是否有“hello”的存在,發現是沒有的,於是在常量池中創建“hello”對象,並將常量池中的引用賦值給str1;第二個字面量 String str2 = "hello",在常量池中檢測到該對象了,直接將引用賦值給str2。

然后是new的方式創建字符串

String a = new String("abc");

new這個關鍵字,毫無疑問會在堆中分配內存,創建一個String類的對象。因此,a這個在棧中的引用指向的是堆中的這個String對象的。

然后,因為"abc"是個常量,所以會去常量池中找,有沒有這個常量存在,沒的話分配一個空間,放這個"abc"常量,並將這個常量對象的空間地址給到堆中String對象里面;如果常量池中已經有了這個常量,就直接用那個常量池中的常量對象的引用唄,就只需要創建一個堆中的String對象。

看下這個圖:(圖片來自https://blog.csdn.net/MOU_IT/article/details/78312399)

所以這個例子:

public static void main(String[] args) {
        
    String s1 = new String("hello");
        
    String s2 = "hello";
        
    String s3 = new String("hello");

    System.out.println(s1 == s2);// false
        
    System.out.println(s1.equals(s2));// true
   
    System.out.println(s1 == s3);//false
}

第一個輸出為false,因為==比較的是引用的地址,s2指的是常量池中常量對象的地址,而s1指的是堆中String對象的地址,肯定不同。

然后第二個為true,因為jdk重寫了equals()方法,比較的是字符串的內容。

第三個輸出為false,原因是每個String對象都是不同的,所以引用指向的堆地址肯定也不同,所以false。

三、關於“+”運算符

純常量相加:

String s1 = "hello" + "word";
String s2 = "helloword";
System.out,println(s1 == s2);//true

這個的輸出是true,意味着"helloword"和"hello" + "word"的地址是一樣的。

但我們之前在《thinking in Java》中看到的是說JVM為了優化這個字符串相加的過程,在“+”這個操作符的重載中自動引入了StringBuilder類喔。

那s2顯然應該是常量池中"helloword"這個常量對象的引用,那這個s1不應該是StringBuilder調用toString方法后產生的堆中的String對象的引用嗎?

查了很多文章,這篇告訴了我們原因——https://www.cnblogs.com/vincentl/p/9600093.html

總結下就是:

  兩個或者兩個以上的字符串常量相加,在預編譯的時候“+”會被優化,相當於把兩個或者兩個以上字符串常量自動合成一個字符串常量.
  字符串常量相加,不會用到StringBuilder對象,有一點要注意的是:字符串常量和字符串是不同的概念,字符串常量儲存於方法區(總之就常量池),而字符串儲存於堆(heap)。

而非純常量的字符串相加的

像是字符串相加表達式中帶變量的那種的話,就是JVM會自動創建一個StringBuilder然后再調用append()方法最后再調用toString()方法返回的方式了,所以在堆中會有個String對象,引用指向的是堆中的對象的地址。

所以相加出來的結果,是不會被加到常量池中的。

String s1 = new String("he")+new String("llo"); 
  1. 這個代碼中,首先,new String("he"),先在常量池中看,發現沒有這個"he"常量,於是建一個,然后再在堆中創建一個String的對象(但沒引用,很快被gc的)。
  2. 加法,暗中new了StringBuilder,調用append方法。
  3. new String("llo")一樣的道理,堆中一個String對象,常量池中"llo"常量對象。
  4. StringBuilder的append方法搞定后,調用toString()方法,具體是new一個String對象,也就是現在是一個堆中的String對象,內容是"hello",但注意這個hello沒有在常量池中創建!!其實可以理解因為沒有出現過一次"hello",拼接是通過StringBuilder的append方法完成的。

 

總之:對於所有包含new方式新建對象(包括null)和變量形式 的“+”連接表達式,它所產生的新對象都不會被加入字符串池中。

再看個例子:

 String s0 = "ab"; 
 final String s1 = "b"; 
 String s2 = "a" + s1;  
 System.out.println((s0 == s2)); //result = true

這個不是帶變量的相加嗎,不應該是返回一個堆上的引用嗎?

這是因為final修飾的s1在編譯期就可以識別,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。所以此時的"a" + s1和"a" + "b"效果是一樣的。故上面程序的結果為true。

 

四、String的intern()方法

看書時的疑惑

在讀JVM的時候,在描述方法區和運行時常量池溢出的章節里面提到了String.intern()方法。

這是一個native的方法,書上是這樣描述它的作用的:如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符添加到常量池中,並返回此String對象的引用。

 

並提到,在JDK1.6及其之前的版本,由於常量池分配在永久代內,我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區的大小從而間接限制常量池的容量。

 

不僅如此,在intern方法返回的引用上,JDK1.6和JDK1.7也有個地方不一樣,來看看書本上給的例子:

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);
}

這段代碼在JDK1.6中,會得到兩個false,在JDK1.7中運行,會得到一個true和一個false。

 

書上說,產生差異的原因是:在JDK1.6中,intern()方法會把首次遇到的字符串實例復制到永久代中,返回的也是永久代中這個字符串實例的引用,而由StringBuilder創建的字符串實例在Java堆上,所以必然不是同一個引用,將返回false。

而JDK1.7的intern()不會再復制實例,只是在常量池中記錄首次出現的實例的引用,因此intern()返回的引用和StringBuilder創建的那個字符串的實例是同一個。對str2比較返回false是因為"java"這個字符串在執行StringBuilder.toString()之前就已經出現過,字符串常量池中已經有它的引用了,不符合“首次出現”的原則,而“計算機軟件”這個字符串則是首次出現的,因此返回true。

 

剛開始看這個我是一臉懵,查了很多資料還有看了很多關於String內存的知識我才搞懂這個。

stringTable的小說明

這里先再提一下字符串常量池,實際上,為了提高匹配速度,也就是為了更快地查找某個字符串是否在常量池中,Java在設計常量池的時候,還搞了張stringTable,這個有點像我們的hashTable,根據字符串的hashCode定位到對應的桶,然后遍歷數組查找該字符串對應的引用。如果找得到字符串,則返回引用,找不到則會把字符串常量放到常量池中,並把引用保存到stringTable了里面。

在JDK7、8中,可以通過-XX:StringTableSize參數StringTable大小

 

 

jdk1.6及其之前的intern()方法

在JDK6中,常量池在永久代分配內存,永久代和Java堆的內存是物理隔離的,執行intern方法時,如果常量池不存在該字符串,虛擬機會在常量池中復制該字符串,並返回引用;如果已經存在該字符串了,則直接返回這個常量池中的這個常量對象的引用。所以需要謹慎使用intern方法,避免常量池中字符串過多,導致性能變慢,甚至發生PermGen內存溢出。

看一個圖片來理解下:(圖片來自https://blog.csdn.net/soonfly/article/details/70147205)

當然,這個常量池和堆是物理隔離的。

總之就是,要抓住“復制”這個字眼,常量池中存的是內容為"abc"的常量對象。

 

看個詳細點的例子:

   public static void main(String[] args) {
        String a = new String("haha");
        System.out.println(a.intern() == a);//false
    }

首先,見到"haha",產量池中沒有這個常量,所以會在常量池中放下這個常量對象,底層是通過ldc命令,"haha"被添加到字符串常量池,然后在stringTable中添加該常量的引用(引用好像是這個String對象中的char數組的地址),而a這個引用指向的是堆中這個String對象的地址,所以肯定是不同的。(而且一個在堆,一個在方法區中)。

 

jdk1.7的intern()方法

JDK 1.7后,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於,如果在常量池找不到對應的字符串,則不會再將字符串拷貝到常量池,而只是在常量池中生成一個對原字符串的引用。簡單的說,就是往常量池放的東西變了:原來在常量池中找不到時,復制一個副本放到常量池,1.7后則是將在堆上的地址引用復制到常量池。

當然這個時候,常量池被從方法區中移出來到了堆中。

看個圖:

(圖片來自https://blog.csdn.net/soonfly/article/details/70147205)

所以再看回我們書上的那個例子

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);
}

這個例子在jdk1.7中的結果是true和false。第一個輸出中,因為“計算機軟件”這個字符串常量,是沒有出現過在常量池中的,所以調用intern()方法的時候,會在常量池中生成一個"計算機軟件"的引用,注意是引用哦!

而str1所指向的也是這個堆對象的引用,所以第一個是true。

而第二個,首先查資料發現,由於JVM的 特殊性在JVM啟動的時候調用了一些方法,在常量池中已經生成了“java”字符串常量。

所以,str2指向的是堆中的String對象,內容是"java",而這個str2調用intern的時候,常量池中會發現已經有了這個常量對象,所以會返回這個已經存在了的"java"常量對象的引用,那肯定呵str2引用指向的堆地址是不同的,所以false。

 

再看一個例子:

String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);//true

這個返回true的原因也一樣,str2的時候,只有一個堆的String對象,然后調用intern,常量池中沒有“str01”這個常量對象,於是常量池中生成了一個對這個堆中string對象的引用。

然后給str1賦值的時候,因為是帶引號的,所以去常量池中找,發現有這個常量對象,就返回這個常量對象的引用,也就是str2引用所指向的堆中的String對象的地址。

所以str2和str1指向的是同一個東西,所以為true。

 

 

參考文章:

基本就是圖片所引用的博客中的相關內容,在每張圖片旁邊都有說明復制的來源,這里就不再引述了。


免責聲明!

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



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