- 首先我要提前說明的一點是,這篇文章是我自己的了解,而且其中涉及了一些JVM指令,但是自己沒有學過這些東西,完全是靠自己的感覺在寫,是以我感覺本片文章會有些漏洞,是以您隻可以做一個參考,我希望您發現不對的地方即使指正,非常感謝
- 這篇是考慮再三冒死拿出來給大家看的,因為一直放在我的筆記對錯我自己完全不知道,是以孬活着不如快樂一死,接收噴,但請帶上您的理由,嘻嘻
String繼承關系
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence{
....
}
- 到這我們可以看到String類不可以被繼承,因為final修飾,是以他的方法自然不可以被重寫,然後String可以進行序列化,比較,以及他實作了
字元序列接口CharSequence
- 總結:String可序列化,可比較,emmm...是個字元序列
String的存儲實作
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence{
//之前版本的JDK的String實作是char數組,需要兩個位元組
//而在之後更新的JDK中實作為byte數組,加上一個下面的coder标志來表示字元
private final byte[] value;
//用于對value的byte進行編碼的編碼辨別符,即使用什麼編碼對value進行編碼
//支援的編碼有LATIN1即ISO-8859-1單位元組編碼和UTF-16雙位元組編碼
//如果value中儲存的字元串都可以用LATIN1儲存,那麼coder=0,否則就使用UTF-16儲存,coder=1
private final byte coder;
//緩存的hash值,預設為0
private int hash;
//數字代表編碼
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
}
- String在JDK8中存儲的形式還是
的,這個變化在JDK9中發生改變的,這個改變使字元串能夠占用更少的空間,因為原來實作的數組是char,是2位元組長度,那麼在更改為byte數組後,每個元素隻有一個位元組的長度,是以節省了一半的空間(并不準确,但是肯定比之前的char節省,下面介紹)private final char value[]
- 比如之前存儲
單詞,how
數組是這樣的char[]
[0][h][0][o][0][w] //之後byte單位元組存儲 [h][o][w]
- 如下圖是JDK8和JDK11中分别存儲how後的char[]數組内的情況

- 是以從上面看到,在存儲字母的時候,也就是單位元組可以存放一個字母的時候,存儲效率達到了最高,這時候就真的是之前
存儲的一半了,但是中國漢字不止占一個位元組,即一個byte存不下一個漢字了,這時候還是需要2位元組去存儲的,比如JDK8和JDK11分别存儲char[]
,如下圖期待a
- 如上圖,由于單位元組存不下漢字,是以
編碼辨別符改為了coder
,即1
UTF-16
- 對于coder可以這樣做一個實驗,如下
String str = new String("xx"); //當你在構造器打斷點的時候,此時coder=0 String str = new String("中國"); //coder=1
- 是以從Java9 的 String 預設是使用了上述緊湊的空間布局的
- 這一改變,也直接影響了String.length()方法
public int length() { //即如果是 LATIN-1 編碼,則右移0位.數組長度即為字元串長度.而如果是 UTF16 編碼,則右移1位,數組長度的二分之一為字元串長度 return value.length >> coder(); } byte coder() { //COMPACT_STRINGS預設為true //這個變量就代表了String一開始是否使用緊湊布局,這個參數由JVM注入,隻能通過虛拟機參數更改 //意思就是如果是緊湊布局的話,那麼我們就使用coder作為傳回值,coder會根據你存的string的内容變化 //如果是False就是放棄緊湊布局,那麼就是用雙位元組進行存儲内容 return COMPACT_STRINGS ? coder : UTF16; }
- 既然String子層存儲發生了變化,那麼相關的
和StringBuilder
也發生了變化,如下是他們兩個類的父類StringBuffer
abstract class AbstractStringBuilder implements Appendable, CharSequence { byte[] value; byte coder; }
- 總結:String在JDK9之前使用char[]儲存資料,在JDK9開始使用byte[]儲存資料,并有一個coder标志符,來表示資料是用哪一種編碼儲存的,以友善之後的方法進行區分對待,并且相關的String類都發生了改變
String初始化過程
- 從
構造器開始new String(char[] ch)
//斷點代碼 public static void main(String[] args) { char[] chars = {'A', 'B'}; String str = new String(chars); System.out.println(str); } //String構造器 public String(char value[]) { this(value, 0, value.length, null); } //String包級别構造器 String(char[] value, int off, int len, Void sig) { //這的注釋可以過一眼,等你看完下面的流程後,你就知道這是什麼作用了 if (len == 0) { this.value = "".value; this.coder = "".coder; return; } //COMPACT_STRINGS預設為true,即代表啟用壓縮,即使用單位元組編碼 if (COMPACT_STRINGS) { //compress裡面判斷如果char數組存在 value > 0xFF 的值時,就傳回null, 0xFF=255 //如果内容全部小于0xFF,即代表可以全部采用單位元組編碼 //那麼傳回值就不是null,那麼直接指派給String類中屬性就行了 byte[] val = StringUTF16.compress(value, off, len); if (val != null) { //直接指派給value屬性,單位元組編碼初始化完畢 this.value = val; this.coder = LATIN1; return; } } //到這就代表上面遇到了不能直接單位元組編碼的String了,然後就開始采用雙位元組編碼 this.coder = UTF16; //然後将要儲存的String用UTF16編碼即可,到這就初始化完畢 this.value = StringUTF16.toBytes(value, off, len); } public static byte[] compress(char[] val, int off, int len) { //這個就是存放char轉到byte後的資料的臨時數組 byte[] ret = new byte[len]; //内部調用,裡面判斷是否c>0xFF,如果都小于,就代表可以全部單位元組編碼,傳回值就==len //如果遇到了c>0xFF的情況,那麼這個條件不會成立 if (compress(val, off, ret, 0, len) == len) { //到這就代表已經儲存進了byte數組内了,傳回就可以 return ret; } //這就代表需要儲存的String不能直接單位元組編碼 return null; } public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { char c = src[srcOff]; //判斷char中的每個是否value > 0xFF if (c > 0xFF) { //如果發現c>0xFF那麼len指派為0,跳出,是以len傳回值也為0,是以會造成上層判斷為false len = 0; break; } //如果不遇到break,說明要儲存的String,可以直接用單位元組編碼,循環完成了,即char也儲存到了byte了 dst[dstOff] = (byte)c; //指針++ srcOff++; dstOff++; } return len; }
- JDK8中的初始化
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
- 到這一個String的基本的構造過程就寫完了,這一部分掰扯了好半天,全是自己的了解,如果有分析的不對,請及時指正
- 總結:已經抛棄了JDK8中的系統拷貝(2位元組),轉而使用字元編碼來差別初始化(1位元組 or 2位元組)
String中的常用方法實作
- 使用方法就不多說了,來看一下他們的實作:
,,substring
replace
//截取字元串 public String substring(int beginIndex, int endIndex) { int length = length(); //檢查是否越界 checkBoundsBeginEnd(beginIndex, endIndex, length); //截取的長度 int subLen = endIndex - beginIndex; if (beginIndex == 0 && endIndex == length) { return this; } //根據編碼來區分截取字元,注意他們的方法是!!newString!!,是以不用跟進去也知道他是建立一個新的子串 return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen) : StringUTF16.newString(value, beginIndex, subLen); }
//替換字元串 public String replace(char oldChar, char newChar) { if (oldChar != newChar) { //StringLatin1這裡面的方法有點長,如下 String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar) : StringUTF16.replace(value, oldChar, newChar); if (ret != null) { return ret; } } return this; } //因為return可以直接傳回結果,是以我們直接看傳回就好了,下面是精簡後的程式,詳細程式我看不懂..嘻嘻 //看到都是newString傳回的 public static String replace(byte[] value, char oldChar, char newChar) { if (canEncode(oldChar)) { ... if (i < len) { ... return new String(buf, LATIN1); } else { ... return new String(buf, UTF16); } } } return null; // for string to return this; }
- 是以到這我們可以看到傳回的是一個新的子串,而并非對原來的String做任何的改變,這也可以作為String是immutable的證據
- 總結:String的任何操作都是傳回一個新的子串,而并非對原來的String做任何修改,String對象一旦被建立就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何change操作都會生成新的對象
常量池
- 這部分内容我不知道怎麼去驗證,是以是參考網上的文章,文末有引用說明
- 字元串在我們程式設計中使用是很多的,是以如果不引入一個機制,那麼除了不重複的字元串緩存外,重複的部分那麼将是無用的,是以這個機制就是常量池,在java中常量池可以保證池中一個字元串僅且隻有這一個,不會出現第二個備份,是以這也就很好的解決了重複字元串的問題
- 當我們建立一個字元串的時候,Java會先去線程池中尋找,如果有就傳回這個字元串,否則就建立一個放入池中,當然這個操作是排除
操作的,僅支援直接可以能夠判斷出變量值的狀态,比如下面new String()
//可以直接得到變量的值 String str1 = "1"; String str2 = "1"; System.out.println(str1 == str2); //true //下面是不能直接得到值的,比如new String() String str1 = "1"; String str2 = new String("1"); System.out.println(str1 == str2); //false
- 對于下面這種情況,剛開始還不了解為啥是true,感覺這個隻能運作期才能擷取值啊,應該是false啊,但是卻不是,後來想了一下,因為
方法中是getNum
也相當于在建立對象,需要到池中搜尋,結果發現有return "1";
,是以比較會傳回true,這隻是我的猜測,我還不知道怎麼去證明,如果不對請指正,謝謝1
- 補充:雖然
比較傳回為true,但是隻是證明是常量池中的一個對象,而這個方法依舊是運作時才會知道其傳回值的,即編譯器無法确定他的具體值getNum
public void test() { String str1 = "1"; String str2 = getNum(); System.out.println("1" == str1); //true System.out.println("1" == str2); //true System.out.println(str1 == str2); //true } private String getNum(){ return "1"; //我的證明是将這裡改為new String("1"),上面會傳回false,是以得出上面的結論 }
- 到這就需要提到兩個概念:
,靜态常量池
動态常量池
- 靜态常量池.即
檔案中的常量池,class檔案中的常量池不僅僅包含字元串(數字)字面量,還包含類,方法的資訊,占用class檔案絕大部分空間*.class
- 運作時常量池,則是jvm虛拟機在完成類裝載操作後,将class檔案中的常量池載入到記憶體中,并儲存在方法區中,我們常說的常量池,就是指方法區中的運作時常量池
- 靜态常量池.即
- 提到上面兩個概念,可以解決這個問題:上面說建立字元串時會在常量池中尋找,那麼
為啥不等于new String("1")
呢?String str = "1"
-
操作其實是建立了一個真正的對象,這個我們都知道,是以這個new出來的對象new
一定會在堆記憶體,我們之前也證明了不管從常用方法還是常量池機制都保證了不會有重複的字元串,是以這的唯一可能就是new出來的對象是引用常量池中的對象的,如果常量池中沒有這個對象,new操作就會先在常量池中建立一個常量,然後再引用他,如下圖1
-
- 對應如下代碼段
String str = new String("1");
String n = "1";
System.out.println(str == n); //false
- 到這就可以看出了,比較
,其一次指向完全不同,是以傳回falsestr = n
- 那怎麼證明new String真的是建立了一個對象一個常量呢 ?(一個new 對象在堆,一個在常量池),我們使用到了
javap -verbose 輸出附加資訊
- 首先如下空實作
public static void main(String[] args) {}
- javap一下,隻截取有用的部分
public class com.qidai.Tests //... Constant pool: //常量池出現,其中沒有我們定義的字元,因為是空實作哈哈 #1 = Methodref #3.#17 // java/lang/Object."<init>":()V #2 = Class #18 // com/qidai/Tests #3 = Class #19 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 LocalVariableTable #9 = Utf8 this #10 = Utf8 Lcom/qidai/Tests; #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 args #14 = Utf8 [Ljava/lang/String; #15 = Utf8 SourceFile #16 = Utf8 Tests.java #17 = NameAndType #4:#5 // "<init>":()V #18 = Utf8 com/qidai/Tests #19 = Utf8 java/lang/Object { public com.qidai.Tests(); //... public static void main(java.lang.String[]); //main方法開始 Code: stack=0, locals=1, args_size=1 0: return //無實作直接傳回 }
- 然後我們在main中加入代碼
String string = new String("MyConstantString");
- 編譯一下再javap看一下
public class com.qidai.Tests Constant pool: #1 = Methodref #6.#22 // java/lang/Object."<init>":()V #2 = Class #23 // java/lang/String #3 = String #24 // MyConstantString #4 = Methodref #2.#25 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Class #26 // com/qidai/Tests #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/qidai/Tests; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 string #19 = Utf8 Ljava/lang/String; #20 = Utf8 SourceFile #21 = Utf8 Tests.java #22 = NameAndType #7:#8 // "<init>":()V #23 = Utf8 java/lang/String #24 = Utf8 MyConstantString //!!!!!!!!!!!類檔案中出現了~~~~~ #25 = NameAndType #7:#28 // "<init>":(Ljava/lang/String;)V #26 = Utf8 com/qidai/Tests #27 = Utf8 java/lang/Object #28 = Utf8 (Ljava/lang/String;)V { public static void main(java.lang.String[]); Code: stack=3, locals=2, args_size=1 //因為有實作了,是以沒有直接傳回 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String MyConstantString !!!!!!!!!!!! 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: return }
- 果然證明了我們說的問題,就是
的時候會建立一個對象和一個常量,至此我們就算驗證結束了,但是我們還可以驗證一個其他的問題:不是說常量池不重複的嘛,那麼我們再定義一個一樣資料的String呢?,是以我們現在main方法中就有兩行内容了,如下new String()
String string = new String("MyConstantString"); String constant = "MyConstantString";
- 編譯此類然後javap檢視
public class com.qidai.Tests Constant pool: #1 = Methodref #6.#23 // java/lang/Object."<init>":()V #2 = Class #24 // java/lang/String #3 = String #25 // MyConstantString #4 = Methodref #2.#26 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Class #27 // com/qidai/Tests #6 = Class #28 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/qidai/Tests; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 string //常量池中會加入Strig引用變量?? #19 = Utf8 Ljava/lang/String; #20 = Utf8 constant //常量池中會加入Strig引用變量?? #21 = Utf8 SourceFile #22 = Utf8 Tests.java #23 = NameAndType #7:#8 // "<init>":()V #24 = Utf8 java/lang/String #25 = Utf8 MyConstantString //僅有一個~~~ #26 = NameAndType #7:#29 // "<init>":(Ljava/lang/String;)V #27 = Utf8 com/qidai/Tests #28 = Utf8 java/lang/Object #29 = Utf8 (Ljava/lang/String;)V { public com.qidai.Tests(); public static void main(java.lang.String[]); Code: stack=3, locals=3, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String MyConstantString 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 初始化方法? 9: astore_1 10: ldc #3 // String MyConstantString 這應該就是咱們定義的constant了 12: astore_2 13: return }
- 因為自己并沒有看過
是以對上面也是一知半解,我們用javap證明了他确實會隻儲存一個常量,但是我們看到常量池中會加入Strig引用變量??這個問題我還不知道怎麼回答,如果您知道此問題,請評論告訴我,謝謝,暫且記住常量池中會出現引用變量吧,畢竟他是真實存在的,但我們現在分析的隻是class類檔案,而JVM中的動态常量池中應該會把他去掉,但這個全局的常量池中應該會有一個與常量池儲存引用的一個機制,要麼怎麼找到常量池中的對象呢?我感覺這個在class檔案中的常量池中出現隻是在描述這個類資訊,也就是說有點定義的意思,這是自己了解的,别信...我說真的...自己沒把握...深入了解Java虛拟機
- 好了知道了這些内容,我們還需要知道JVM在編譯的時候會進行編譯優化的,比如宏變量的替換,比如上面的可确定的變量直接寫為确定值了,比如
public static void main(String[] args) { String str = "1"+"2"+"3"; String method = getNum(); } private static String getNum() {return "1";}
- 編譯檢視class檔案,IDEA就可以直接點選生成的class檔案進行檢視
public static void main(String[] args) { String str = "123"; //直接替換為可确定值 String method = getNum(); //這是不可确定的 } private static String getNum() {return "1";}
- 好了知道了會進行編譯優化的話,我們來看幾個執行個體
public static void main(String[] args) { String s0= "helloworld"; //直接常量池 helloworld String s1= new String("helloworld"); //堆helloworld+引用常量池helloworld //javap看是hello和一個world常量 String s2= "hello" + new String("world"); System.out.println("===========test4============"); //s0常量引用不等于s1的堆引用 System.out.println( s0==s1 ); //false //s0的常量引用不等于s2的堆引用和常量引用 System.out.println( s0==s2 ); //false //s1不等于s2,因為s2生成了兩個一個hello和一個world System.out.println( s1==s2 ); //false }
- 如上一切都很正常,唯獨s2比較特殊,javap檢視constant pool中有三個常量:
helloworld
world
,這個hello\u0001
有人知道是什麼東西嗎??連接配接符?\u0001
- 如上一切都很正常,唯獨s2比較特殊,javap檢視constant pool中有三個常量:
- 早期版本的常量池是放入永生代的,但是永生代是大小有限制的,是以在之後版本中将常量池放入了堆中,避免了永久代沾滿的問題,甚至永久代在JDK8中被替換為METASpace中繼資料區替代了
- 總結:String的常量池保證隻有一個唯一的字元串,不會發生重複,并且newString操作是先去判斷常量池中是否有常量,有則引用,否則建立并且JVM會自動編譯優化,将可以直接确定下來的值替換掉原來的值
intern
- 這個是一個可以擴充常量池内常量的個數的方法,即new String的時候,他會去建立一個常量在常量池,本文之前都是這麼說的,但是在網上的文章中說到這個建立常量池的動作是lazy的,是以堆中有了對象而不一定常量池中也會有,即字元串字面量會進入到目前類的運作時常量池,不會進入全局的字元串常量池,是以這個方法其實是觸發lazy機制,使其将資料放入常量池,下面有一篇美團的分享貼,可以看一下,自己不太了解就不多比比了
- intern是顯示排重機制,但是每次調用就很麻煩,在jdk8u20推出了G1 GC下的字元串排重,他是通過将相同資料的字元串指向同一份資料來做到的,是JVM底層改變,并不涉及API的修改
- G1 GC排重預設是關閉的,需要指定
-XX:+UseStringDeduplication
- 總結:可以擴充常量池内常量的個數,在Java8特定版本後,JVM就會幫我們做這件事
String,StringBuffer,StringBuilder
- String是java語言非常基礎和重要的類,提供了構造和管理字元串的各種基本邏輯,他是典型的immutable類,被聲明成為final class,所有屬性也都是final的,由于它的不可變現,類似拼接裁剪動作都會産生新的String對象
- StringBuffer是解決拼接太多造成很多String對象的類,本質是一個線程安全的可修改字元序列,他保證了線程安全,但同時帶來了額外的性能開銷
- StringBuilder是jdk5新增的,和StringBuffer類似,隻是這個不是線程安全的
- String是Immutable類的典型實作,他保證了線程安全,因為無法對内部資料進行更改
- StringBuffer顯示的一些細節,他的線程安全是通過把各種修改資料的方法加上sync實作的,
- 為了實作修改字元序列的目的,StringBuffer和StringBuilder底層都是利用可修改的數組,二者都內建了AbstractStringBuilder,之間的差別僅僅是方法是否加了sync
- 内部數組的大小的實作是:構造時初始字元串長度加16,是以可以根據自己的需要建立合适的大小
- 在java8中字元串的拼接操作會轉換為StringBuilder操作,而java9提供了StringConcatFactory,作為統一入口