學習String相關知識點的一些總結

由一個例子引出今天的豬腳。


        String ss = "ab";
        String s1 = "a";
        String s2 = "b";
        String s3 = s1 + s2;
        String s33 = "ab";
        String s4 = new String("a" + "b");

        System.out.println(ss == s3);//false
        System.out.println(s3 == s33);//false
        System.out.println(ss == s33);//true
        System.out.println("---------------------------------------");
        System.out.println(s3 == s1+s2);//false
        System.out.println(s3 == s4);//false
        System.out.println(ss == s4);//false
        System.out.println("---------------------------------------");
        String s5 = "a" + "b";
        System.out.println(s4 == s5);//false
        String s51 = "a" + "b";
        System.out.println(s51 == s5);//true
        System.out.println("---------------------------------------");
        final String s6 = "a";
        final String s7 = "b";
        System.out.println(ss == s5);//true
        System.out.println(ss == s6+s7); //true

執行結果 :
false
false
true

false
false
false

false
true

true
true

下面從結果來看分析一下
首先,我們都知道 == 、equals這兩種比較是否相等的方式,== 的比較兩個基本類型 那麼判斷 true/false 依據是值是否相等,對於非基本類型比較的是內存的地址是否相同。
ss == s3 false 說明變量String s3 =“a”+“b” 指向的地址不等於 String ss = “ab”;
String ss = "ab" ss 直接指向的是常量池中的“ab”的地址。 而 s3 並不是直接指向常量池中的“ab”地址,故而 “a”+“b” 這一過程 存在類似 new 對象的過程。指向的是其在堆中的地址。

接下來 ss == s33 true 這個說明都是指向的同一地址,都執向了常量池中的 “ab”。

 String s4 = new String("a" + "b");
 String ss = "ab";

s4 == ss false
這個可以結合下面這個常見的問題來解釋:
衆所周知,String 有兩種聲明方式

String a = "abc";
String b = new String("abc");

這兩種聲明方式是有區別的,第一種方式是棧內存a 指向 常量池中的“abc”。
第二種方式 是在堆中開闢內存創建了一個String 對象,然後這個對象指向的是線程池中的“abc”。

換一種說法就是下面這個問題的答案。
String a = new String(“abc”) 創建多少個對象?

  1. 在常量池中查找是否有“abc”對象
    有則返回對應的引用實例
    沒有則創建對應的實例對象
  2. 在堆中 new 一個 String(“abc”) 對象
  3. 將對象地址賦值給str4,創建一個引用

所以,` 常量池中沒有“abc”字面量則創建兩個對象,否則創建一個對象,以及創建一個引用變量,往往會提出這樣的變式題:
String str1 = new String(“A”+“B”) ; 會創建多少個對象?
String str2 = new String(“ABC”) + “ABC” ; 會創建多少個對象?

str1:
字符串常量池:“AB” : 1個
堆:new String(“AB”) :1個
引用: str1 :1個
總共 : 5個

str2 :
字符串常量池:“ABC” : 1個
堆:new String(“ABC”) :1個
引用: str2 :1個
總共 : 3個

下面用圖形表示一下String a = new String(“abc”)在jvm內存中的分佈,
在這裏插入圖片描述

JVM的內存區域

JVM的內存區域分爲 程序計數器、 虛擬機棧、 本地方法區、 堆、 方法區 5部分。
這5部分又可以根據是否線程私有,歸爲兩類。
在這裏插入圖片描述

線程私有:程序計數器、虛擬機棧、本地方法區。
線程共享:堆、方法區。

程序計數器(線程私有):一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的程序計數器 ,這類內存也稱爲“線程私有”的內存。
正在執行 java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址)。

虛擬機棧(線程私有):是描述java方法執行的內存模型 ,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息 。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨着方法調用而創建,隨着方法結束而銷燬——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。

本地方法區(線程私有):本地方法區和 Java Stack 作用類似, 區別是虛擬機棧爲執行 Java 方法服務, 而本地方法棧則爲Native 方法服務 , 如果一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一。

堆(Heap- 線程共享)-運行時數據區:是被線程共享的一塊內存區域,創建的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域 。由於現代 VM 採用分代收集算法, 因此 Java 堆從 GC 的角度還可以細分爲: 新生代( Eden 區 、 From Survivor 區 和 To Survivor 區 )和老年代。

方法區/ 永久代(線程共享):即我們常說的永久代(Permanent Generation), 用於存儲被 JVM 加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據 . HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內存,而不必爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中 。 Java 虛擬機對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求,這樣纔會被虛擬機認可、裝載和執行。
常量池的設計保證了String 類型的唯一性,避免了重複開闢空間,節省了內存。這裏可以擴展一個問題,String 爲什麼被設計成不可變的?

另java8開始 沒有永久代一說,改成元空間。
Java 8: 從永久代(PermGen)到元空間(Metaspace)

String類型爲什麼不可變

下面來聊一下的String 爲什麼被設計成不可變的?

String是Java中最常用的類,是不可變的(Immutable), 那麼String是如何實現Immutable呢,String爲什麼要設計成不可變呢?
先翻看String 底層實現:

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
    
    //other codes
}

String 的底層實現是依靠 char value[] 數組,既然依靠的是基礎類型變量,那麼他一定是可變的, String 之所以不可變,是因爲 Java 的開發者通過技術實現,隔絕了使用者對 String 的底層數據的操作。

String 不可變的技術實現
String 類由關鍵字 final 修飾,說明該類不可繼承
char value[] 屬性也被 final 所修飾,說明 value 的引用在創建之後,就不能被改變
以上兩點並不能完全實現 String 不可變 ,原因在於:

final int[] value={1,2,3}
      int[] another={4,5,6};
value=another;    // 編譯器報錯,final不可變

value 被 final 修飾,只能保證引用不被改變,但是 value 所指向的堆中的數組,纔是真實的數據,只要能夠操作堆中的數組,依舊能改變數據。

final int[] value={1,2,3};
value[2]=100;  //這時候數組裏已經是{1,2,100}

所有的成員屬性均被 private 關鍵字所修飾
爲了實現 String 不可變,關鍵在於Java的開發者在設計和開發 String 的過程中,沒有暴露任何的內部成員,與此同時 API 的設計是均沒有操作 value 的值 , 而是採用 new String() 的方式返回新的字符串,保證了 String 的不可變。
JDK String API 源碼:

public static String valueOf(char c) {
        char data[] = {c};
        return new String(data, true);  //採用 new String() 的方式返回新的字符串
    }
    
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);  //採用 new String() 的方式返回新的字符串
    }

整個String設成final禁止繼承,避免被其他人繼承後破壞。所以String是不可變的關鍵都在底層的實現,而不是一個final。

爲什麼會將 String 設計爲不可變?

安全
保證線程安全,在併發場景下,多個線程同時讀寫資源時,會引競態條件,由於 String 是不可變的,不會引發線程的問題而保證了線程的安全。
HashCode,當 String 被創建出來的時候,hashcode也會隨之被緩存,hashcode的計算與value有關,若 String 可變,那麼 hashcode 也會隨之變化,針對於 Map、Set 等容器,他們的鍵值需要保證唯一性和一致性,因此,String 的不可變性使其比其他對象更適合當容器的鍵值。
性能
當字符串是不可變時,字符串常量池纔有意義。字符串常量池的出現,可以減少創建相同字面量的字符串,讓不同的引用指向池中同一個字符串,爲運行時節約很多的堆內存。若字符串可變,字符串常量池失去意義,基於常量池的String.intern()方法也失效,每次創建新的 String 將在堆內開闢出新的空間,佔據更多的內存。

字符串常量池

在《深入理解java虛擬機》這本書上是這樣寫的:對於HotSpot虛擬機,根據官方發佈的路線圖信息,現在也有放棄永久代並逐步的改爲採用Native Memory來實現方法區的規劃了,在目前已經發布的JDK1.7的HotSpot中,已經把原來存放在方法區中的字符串常量池移出。根據查閱的資料顯示在JDK1.7以後的版本中字符串常量池移到堆內存區域;同時在jdk1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域。

在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程中速度更快,更節省內存,都提供了一種常量池的概念。常量池就類似一個JAVA系統級別提供的緩存。

java中兩種創建字符串對象的方式的分析。

它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
  • 如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);  true

採用字面值的方式創建一個字符串時,JVM首先會去字符串池中查找是否存在"abc"這個對象,如果不存在,則在字符串常量池中創建"abc"這個對象,然後將池中"abc"這個對象的引用地址返回給"abc"對象的引用s1,這樣s1會指向字符串常量池中"abc"這個字符串對象;如果存在,則不創建任何對象,直接將池中"abc"這個對象的地址返回,賦給引用s2。因爲s1、s2都是指向同一個字符串池中的"abc"對象,所以結果爲true。

String s3 = new String("xyz");
String s4 = new String("xyz");
System.out.println(s3==s4); false

採用new關鍵字新建一個字符串對象時,JVM首先在字符串池中查找有沒有"xyz"這個字符串對象,如果有,則不在池中再去創建"xyz"這個對象了,直接在堆中創建一個"xyz"字符串對象,然後將堆中的這個"xyz"對象的地址返回賦給引用s3,這樣,s3就指向了堆中創建的這個"xyz"字符串對象;如果沒有,則首先在字符串池中創建一個"xyz"字符串對象,然後再在堆中創建一個"xyz"字符串對象,然後將堆中這個"xyz"字符串對象的地址返回賦給s3引用,這樣,s3指向了堆中創建的這個"xyz"字符串對象。s4則指向了堆中創建的另一個"xyz"字符串對象。s3 、s4是兩個指向不同對象的引用,結果當然是false。

Intern的實現原理

/**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

String#intern方法中看到,這個方法是一個 native 的方法,但註釋寫的非常明瞭。“ 當調用 intern方法時,如果池已經包含一個等於此String對象的字符串(用equals(oject)方法確定),則返回池中的字符串。否則,將此String對象添加到池中,並返回此String對象的引用。

public void test(){
        //發現原來是在JVM啓動的時候調用了一些方法,在常量池中已經生成了"java"字符串常量,
         String s2 = new String("ja") + new String("va");
         String s3 =  s2.intern();
         String s4 = "java";
         System.out.println(s2 == s3);fasle
         System.out.println(s3 == s4);true
    }

JDK1.8版本中,String常量池已經從方法區中的運行時常量池分離到堆中了,那麼在堆中的String常量池裏存的是String對象還是引用呢?

相關資料:

面試別再問我String了
String類型爲什麼不可變
String字符串“真正存儲位置”
String的內存模型,爲什麼String被設計成不可變的
深入解析String#intern
JDK1.8版本java字符串常量池裏存的是String對象還是引用?

如果你也熱衷技術歡迎加羣一起進步:230274309 。 一起分享,一起進步!少划水,多曬乾貨!!歡迎大家!!!(進羣潛水者勿加)

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章