JVM——字符串常量池詳解


技術公眾號:后端技術解憂鋪
關注微信公眾號:CodingTechWork,一起學習進步。

引言

  在Java開發中不管是前后端交互的JSON串,還是數據庫中的數據存儲,我們常常需要使用到String類型的字符串。作為最常用也是最基礎的引用數據類型,JVM為String提供了字符串常量池來提高性能,本篇文章我們一起從底層JVM中認識並學習字符串常量池的概念和設計原理。

字符串常量池由來

  在日常開發過程中,字符串的創建是比較頻繁的,而字符串的分配和其他對象的分配是類似的,需要耗費大量的時間和空間,從而影響程序的運行性能,所以作為最基礎最常用的引用數據類型,Java設計者在JVM層面提供了字符串常量池。

實現前提

  1. 實現這種設計的一個很重要的因素是:String類型是不可變的,實例化后,不可變,就不會存在多個同樣的字符串實例化后有數據沖突;
  2. 運行時,實例創建的全局字符串常量池中會有一張表,記錄着長相持中每個唯一的字符串對象維護一個引用,當垃圾回收時,發現該字符串被引用時,就不會被回收。

實現原理

  為了提高性能並減少內存的開銷,JVM在實例化字符串常量時進行了一系列的優化操作:

  1. 在JVM層面為字符串提供字符串常量池,可以理解為是一個緩存區;
  2. 創建字符串常量時,JVM會檢查字符串常量池中是否存在這個字符串;
  3. 若字符串常量池中存在該字符串,則直接返回引用實例;若不存在,先實例化該字符串,並且,將該字符串放入字符串常量池中,以便於下次使用時,直接取用,達到緩存快速使用的效果。
	String str1 = "abc";
	String str2 = "abc";
	System.out.println("str1 == str2: " + (str1 == str2)); //結果:str1 == str2: true

字符串常量池位置變化

方法區

  提到字符串常量池,還得先從方法區說起。方法區和Java堆一樣(但是方法區是非堆),是各個線程共享的內存區域,是用於存儲已經被JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
  很多人會把方法區稱為永久代,其實本質上是不等價的,只不過HotSpot虛擬機設計團隊是選擇把GC分代收集擴展到了方法區,使用永久代來代替實現方法區。其實,在方法區中的垃圾收集行為還是比較少的,這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,但是這個區域的回收總是不盡如人意的,如果該區域回收不完全就會出現內存泄露。當然,對於JDK1.8時,HostSpot VM對JVM模型進行了改造,將元數據放到本地內存,將常量池和靜態變量放到了Java堆里。

元空間

   JDK 1.8, HotSpot JVM將永久代移除了,使用本地內存來存儲類的元數據信息,即為元空間(Metaspace)

  所以,字符串常量池的具體位置是在哪里?當然這個我們后面需要區分jdk的版本,jdk1.7之前,jdk1.7,以及jdk1.8,因為這些版本中,字符串常量池因為方法區的改變而做了一些變化。

JDK1.7之前

  在jdk1.7之前,常量池是存放在方法區中的。
在這里插入圖片描述

JDK1.7

  在jdk1.7中,字符串常量池移到了堆中,運行時常量池還在方法區中。
在這里插入圖片描述

JDK1.8

  jdk1.8刪除了永久代,方法區這個概念還是保留的,但是方法區的實現變成了元空間,常量池沿用jdk1.7,還是放在了堆中。這樣的效果就變成了:常量池和靜態變量存儲到了堆中,類的元數據及運行時常量池存儲到元空間中。
在這里插入圖片描述
  為啥要把方法區從JVM內存(永久代)移到直接內存(元空間)?主要有兩個原因:

  1. 直接內存屬於本地系統的IO操作,具有更高的一個IO操作性能,而JVM的堆內存這種,如果有IO操作,也是先復制到直接內存,然后再去進行本地IO操作。經過了一系列的中間流程,性能就會差一些。非直接內存操作:本地IO操作——>直接內存操作——>非直接內存操作——>直接內存操作——>本地IO操作,而直接內存操作:本地IO操作——>直接內存操作——>本地IO操作
  2. 永久代有一個無法調整更改的JVM固定大小上限,回收不完全時,會出現OutOfMemoryError問題;而直接內存(元空間)是受到本地機器內存的限制,不會有這種問題。

變化

  1. 在JDK1.7前,運行時常量池+字符串常量池是存放在方法區中,HotSpot VM對方法區的實現稱為永久代。
  2. 在JDK1.7中,字符串常量池從方法區移到堆中,運行時常量池保留在方法區中。
  3. 在JDK1.8中,HotSpot移除永久代,使用元空間代替,此時字符串常量池保留在堆中,運行時常量池保留在方法區中,只是實現不一樣了,JVM內存變成了直接內存。

結合代碼

字符串對象創建詳解

代碼示例

	String str1 = "123";
	String str2 = "123";
    String str3 = "123";
    String str4 = new String("123");
    String str5 = new String("123");
    String str6 = new String("123");

結果

str1 == str2:true
str2 == str3:true
str3 == str4:false
str4 == str5:false
str5 == str6:false

jvm存儲示例

在這里插入圖片描述

創建對象流程

對於jvm底層,String str = new String("123")創建對象流程是什么?

  1. 在常量池中查找是否存在"123"這個字符串;若有,則返回對應的引用實例;若無,則創建對應的實例對象;
  2. 在堆中new一個String類型的"123"字符串對象;
  3. 將對象地址復制給str,然后創建一個應用。

注意
若常量池里沒有"123"字符串,則創建了2個對象;若有該字符串,則創建了一個對象及對應的引用。

Q&A

String str ="ab" + "cd";對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(1個對象)"abcd";
  2. 堆:無
  3. 棧:(1個引用)str
    總共:1個對象+1個引用

String str = new String("abc");對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(1個對象)"abc";
  2. 堆:(1個對象)new String("abc")
  3. 棧:(1個引用)str
    總共:2個對象+1個引用

String str = new String("a" + "b");對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(3個對象)"a","b","ab";
  2. 堆:(1個對象)new String("ab")
  3. 棧:(1個引用)str
    總共:4個對象+1個引用

String str = new String("ab") + "ab";對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(1個對象)"ab";
  2. 堆:(1個對象)new String("ab")
  3. 棧:(1個引用)str
    總共:2個對象+1個引用

String str = new String("ab") + new String("ab");對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(1個對象)"ab";
  2. 堆:(2個對象)new String("ab"),new String("ab")
  3. 棧:(1個引用)str
    總共:3個對象+1個引用

String str = new String("ab") + new String("cd");對象個數?

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(2個對象)"ab","cd";
  2. 堆:(2個對象)new String("ab"),new String("cd")
  3. 棧:(1個引用)str
    總共:4個對象+1個引用

String str3 = str1 + str2;對象個數?

String str1 = "ab";
String str2 = "cd";
String str3 = str1 + str2;

分析:若字符串常量池該字符串對象

  1. 字符串常量池:(2個對象)"ab","cd","abcd";
  2. 堆:無
  3. 棧:(3個引用)str1,str2,str3
    總共:2個對象+3個引用

如何指向字符串池中特定的對象?

通過intern()方法。
代碼

        String str1 = "123";
        String str2 = new String("123");
        String str3 = str2;
        System.out.println("str1 == str2:" + (str1 == str2));
        System.out.println("str1 == str3:" + (str1 == str3));

		//通過java.lang.String.intern()方法指定字符串對象
        String str4 = str2.intern();
        System.out.println("str1 == str4:" + (str1 == str4));

結果

str1 == str2:false
str1 == str3:false
str1 == str4:true


免責聲明!

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



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