版權聲明:本文爲博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處連接和本聲明。
本文連接:https://blog.csdn.net/qq_34490018/article/details/82110578
目錄html
JVM相關知識java
String源碼分析數組
Srtring在JVM層解析緩存
String典型案例安全
String被設計成不可變和不能被繼承的緣由性能優化
JVM相關知識多線程
下面這張圖是JVM的體系結構圖:app
下面咱們瞭解下Java棧、Java堆、方法區和常量池:函數
Java棧(線程私有數據區):工具
每一個Java虛擬機線程都有本身的Java虛擬機棧,Java虛擬機棧用來存放棧幀,每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
Java堆(線程共享數據區):
在虛擬機啓動時建立,此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配。
方法區(線程共享數據區):
方法區在虛擬機啓動的時候被建立,它存儲了每個類的結構信息,例如運行時常量池、字段和方法數據、構造函數和普通方法的字節碼內容、還包括在類、實例、接口初始化時用到的特殊方法。在JDK8以前永久代是方法區的一種實現,而JDK8元空間替代了永久代,永久代被移除,也能夠理解爲元空間是方法區的一種實現。
常量池(線程共享數據區):
常量池常被分爲兩大類:靜態常量池和運行時常量池。
靜態常量池也就是Class文件中的常量池,存在於Class文件中。
運行時常量池(Runtime Constant Pool)是方法區的一部分,存放一些運行時常量數據。
下面重點了解的是字符串常量池:
字符串常量池存在運行時常量池之中(在JDK7以前存在運行時常量池之中,在JDK7已經將其轉移到堆中)。
字符串常量池的存在使JVM提升了性能和減小了內存開銷。
使用字符串常量池,每當咱們使用字面量(String s=」1」;)建立字符串常量時,JVM會首先檢查字符串常量池,若是該字符串已經存在常量池中,那麼就將此字符串對象的地址賦值給引用s(引用s在Java棧中)。若是字符串不存在常量池中,就會實例化該字符串而且將其放到常量池中,並將此字符串對象的地址賦值給引用s(引用s在Java棧中)。
使用字符串常量池,每當咱們使用關鍵字new(String s=new String(」1」);)建立字符串常量時,JVM會首先檢查字符串常量池,若是該字符串已經存在常量池中,那麼再也不在字符串常量池建立該字符串對象,而直接堆中複製該對象的副本,而後將堆中對象的地址賦值給引用s,若是字符串不存在常量池中,就會實例化該字符串而且將其放到常量池中,而後在堆中複製該對象的副本,而後將堆中對象的地址賦值給引用s。
下圖是API說明:
翻譯爲:「初始化一個新建立的字符串對象,以便它表示與參數相同的字符序列;換句話說,新建立的字符串是參數字符串的副本。除非須要顯式的原始副本,不然使用此構造函數是沒必要要的,由於字符串是不可變的。」
因爲String字符串的不可變性咱們能夠十分確定常量池中必定不存在兩個相同的字符串。
鑑於String.intern()在API上的說明和new String(「a」)建立字符串(建立了兩個對象,若是字符串常量池存在則是一個對象)在官方API上的說明,我我的認爲字符串常量池存的是字符串對象,固然在JKD7以後,常量池中存儲的多是堆對象的引用,後面會講到。(可用javap -c反編譯便可獲得JVM執行的字節碼內容,javap -verbose 反編譯查看常量池內容)
關於常量池,我會在後面的一篇相關文章中進行解析。。。。
String源碼分析
下面是String類的部分源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** 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;
........
}
首先咱們來看看String類,String類是用final修飾的,這意味着String不能被繼承,並且全部的成員方法都默認爲final方法。
接下來看看String類實現的接口:
java.io.Serializable:這個序列化接口僅用於標識序列化的語意。
Comparable<String>:這個compareTo(T 0)接口用於對兩個實例化對象比較大小。
CharSequence:這個接口是一個只讀的字符序列。包括length(), charAt(int index), subSequence(int start, int end)這幾個API接口,值得一提的是,StringBuffer和StringBuild也是實現了改接口。
最後看看String的成員屬性:
value[] :char數組用於儲存String的內容。
offset :存儲的第一個索引。
count :字符串中的字符數。
hash :String實例化的hashcode的一個緩存,String的哈希碼被頻繁使用,將其緩存起來,每次使用就不必再次去計算,這也是一種性能優化的手段。這也是String被設計爲不可變的緣由之一,後面會講到。
下面是一個String類的一個方法實現:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
能夠發現,最初傳入的String並無改變,其返回的是一個new String(),即新建立的String對象。其實String類的其餘方法也是如此,並不會改變原字符串。這也是String的不可變性,後面會講到。
Srtring在JVM層解析
建立字符串形式
首先形如聲明爲S ss是一個類S的引用變量ss(咱們經常稱之爲句柄,後面JVM相關內容會講到),而對象通常經過new建立。因此這裏的ss僅僅是引用變量,並非對象。
建立字符串的兩種基本形式:
String s1=」1」;
String s2=new String(「1」);
從圖中能夠看出,s1使用」」引號(也是平時所說的字面量)建立字符串,在編譯期的時候就對常量池進行判斷是否存在該字符串,若是存在則不建立直接返回對象的引用;若是不存在,則先在常量池中建立該字符串實例再返回實例的引用給s1。注意:編譯期的常量池是靜態常量池,之後和會講。。。。
再來看看s2,s2使用關鍵詞new建立字符串,JVM會首先檢查字符串常量池,若是該字符串已經存在常量池中,那麼再也不在字符串常量池建立該字符串對象,而直接堆中複製該對象的副本,而後將堆中對象的地址賦值給引用s2,若是字符串不存在常量池中,就會實例化該字符串而且將其放到常量池中,而後在堆中複製該對象的副本,而後將堆中對象的地址賦值給引用s2。注意:此時是運行期,那麼字符串常量池是在運行時常量池中的。。。。
「+」鏈接形式建立字符串(更多能夠查看API):
(1)String s1=」1」+」2」+」3」;
使用包含常量的字符串鏈接建立是也是常量,編譯期就能肯定了,直接入字符串常量池,固然一樣須要判斷是否已經存在該字符串。
(2)String s2=」1」+」3」+new String(「1」)+」4」;
當使用「+」鏈接字符串中含有變量時,也是在運行期才能肯定的。首先鏈接操做最開始時若是都是字符串常量,編譯後將盡量多的字符串常量鏈接在一塊兒,造成新的字符串常量參與後續的鏈接(可經過反編譯工具jd-gui進行查看)。
接下來的字符串鏈接是從左向右依次進行,對於不一樣的字符串,首先以最左邊的字符串爲參數建立StringBuilder對象(可變字符串對象),而後依次對右邊進行append操做,最後將StringBuilder對象經過toString()方法轉換成String對象(注意:中間的多個字符串常量不會自動拼接)。
實際上的實現過程爲:String s2=new StringBuilder(「13」).append(new String(「1」)).append(「4」).toString();
當使用+進行多個字符串鏈接時,其實是產生了一個StringBuilder對象和一個String對象。
(3)String s3=new String(「1」)+new String(「1」);
這個過程跟(2)相似。。。。。。
String.intern()解析
String.intern()是一個Native方法,底層調用C++的 StringTable::intern 方法,源碼註釋:當調用 intern 方法時,若是常量池中已經該字符串,則返回池中的字符串;不然將此字符串添加到常量池中,並返回字符串的引用。
下面咱們來看個案例:
public class StringTest {
public static void main(String[] args) {
// TODO 自動生成的方法存根
String s3 = new String("1") + new String("1");
System.out.println(s3 == s3.intern());
}
}
JDK6的執行結果爲:false
JDK7和JDK8的執行結果爲:true
JDK6的內存模型以下:
咱們都知道JDK6中的常量池是放在永久代的,永久代和Java堆是兩個徹底分開的區域。而存在變量使用「+」鏈接而來的的對象存在Java堆中,且並未將對象存於常量池中,當調用 intern 方法時,若是常量池中已經該字符串,則返回池中的字符串;不然將此字符串添加到常量池中,並返回字符串的引用。因此結果爲false。
JDK7JDK8的內存模型以下:
JDK7中,字符串常量池已經被轉移至Java堆中,開發人員也對intern 方法作了一些修改。由於字符串常量池和new的對象都存於Java堆中,爲了優化性能和減小內存開銷,當調用 intern 方法時,若是常量池中已經存在該字符串,則返回池中字符串;不然直接存儲堆中的引用,也就是字符串常量池中存儲的是指向堆裏的對象。因此結果爲true。
String典型案例
關於equals和== :
(1)對於==,若是做用於基本數據類型的變量(byte,short,char,int,long,float,double,boolean ),則直接比較其存儲的"值"是否相等;若是做用於引用類型的變量(String),則比較的是所指向的對象的地址(便是否指向同一個對象)。
(2)equals方法是基類Object中的方法,所以對於全部的繼承於Object的類都會有該方法。在Object類中,equals方法是用來比較兩個對象的引用是否相等。
(3)對於equals方法,注意:equals方法不能做用於基本數據類型的變量。若是沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;而String類對equals方法進行了重寫,用來比較指向的字符串對象所存儲的字符串是否相等。其餘的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的對象所存儲的內容是否相等。
public class StringTest {
public static void main(String[] args) {
// TODO 自動生成的方法存根
/**
* 情景一:字符串池
* JAVA虛擬機(JVM)中存在着一個字符串池,其中保存着不少String對象;
* 而且能夠被共享使用,所以它提升了效率。
* 因爲String類是final的,它的值一經建立就不可改變。
* 字符串池由String類維護,咱們能夠調用intern()方法來訪問字符串池。
*/
String s1 = "abc";
//↑ 在字符串池建立了一個對象
String s2 = "abc";
//↑ 字符串pool已經存在對象「abc」(共享),因此建立0個對象,累計建立一個對象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一個對象,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
//↑------------------------------------------------------over
/**
* 情景二:關於new String("")
*
*/
String s3 = new String("abc");
//↑ 建立了兩個對象,一個存放在字符串池中,一個存在與堆區中;
//↑ 還有一個對象引用s3存放在棧中
String s4 = new String("abc");
//↑ 字符串池中已經存在「abc」對象,因此只在堆中建立了一個對象
System.out.println("s3 == s4 : "+(s3==s4));
//↑false s3和s4棧區的地址不一樣,指向堆區的不一樣地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地區多不一樣,一個棧區,一個堆區
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
//↑------------------------------------------------------over
/**
* 情景三:
* 因爲常量的值在編譯的時候就被肯定(優化)了。
* 在這裏,"ab"和"cd"都是常量,所以變量str3的值在編譯時就能夠肯定。
* 這行代碼編譯後的效果等同於: String str3 = "abcd";
*/
String str1 = "ab" + "cd"; //1個對象
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11));
//↑------------------------------------------------------over
/**
* 情景四:
* 局部變量str2,str3存儲的是存儲兩個拘留字符串對象(intern字符串對象)的地址。
*
* 第三行代碼原理(str2+str3):
* 運行期JVM首先會在堆中建立一個StringBuilder類,
* 同時用str2指向的拘留字符串對象完成初始化,
* 而後調用append方法完成對str3所指向的拘留字符串的合併,
* 接着調用StringBuilder的toString()方法在堆中建立一個String對象,
* 最後將剛生成的String對象的堆地址存放在局部變量str4中。
*
* 而str5存儲的是字符串池中"abcd"所對應的拘留字符串對象的地址。
* str4與str5地址固然不同了。
*
* 內存中實際上有五個字符串對象:
* 三個拘留字符串對象、一個String對象和一個StringBuilder對象。
*/
String str2 = "ab"; //1個對象
String str3 = "cd"; //1個對象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
//↑------------------------------------------------------over
/**
* 情景五:
* JAVA編譯器對string + 基本類型/常量 是當成常量表達式直接求值來優化的。
* 運行期的兩個string相加,會產生新的對象的,存儲在堆(heap)中
*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67));
//↑str6爲變量,在運行期纔會被解析。
final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 : "+ (str9 == str89));
//↑str8爲常量變量,編譯期會被優化
//↑------------------------------------------------------over
}
}
運行結果:
s1 == s2 : true
s1.equals(s2) : true
s3 == s4 : false
s3.equals(s4) : true
s1 == s3 : false
s1.equals(s3) : true
str1 = str11 : true
str4 = str5 : false
str7 = str67 : false
str9 = str89 : true
String被設計成不可變和不能被繼承的緣由
String是不可變和不能被繼承的(final修飾),這樣設計的緣由主要是爲了設計考慮、效率和安全性。
字符串常量池的須要:
只有當字符串是不可變的,字符串池纔有可能實現。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字符串。倘若字符串對象容許改變,那麼將會致使各類邏輯錯誤,好比改變一個對象會影響到另外一個獨立對象. 嚴格來講,這種常量池的思想,是一種優化手段。
String對象緩存HashCode:
上面解析String類的源碼的時候已經提到了HashCode。Java中的String對象的哈希碼被頻繁地使用,字符串的不可變性保證了hash碼的惟一性。
安全性
首先String被許多Java類用來當參數,若是字符串可變,那麼會引發各類嚴重錯誤和安全漏洞。
再者String做爲核心類,不少的內部方法的實現都是本地調用的,即調用操做系統本地API,其和操做系統交流頻繁,假如這個類被繼承重寫的話,不免會是操做系統形成巨大的隱患。
最後字符串的不可變性使得同一字符串實例被多個線程共享,因此保障了多線程的安全性。並且類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。
學習參考資料:
https://www.cnblogs.com/xiaoxi/p/6036701.html
https://tech.meituan.com/in_depth_understanding_string_intern.html ———————————————— 版權聲明:本文爲CSDN博主「個人書包哪裏去了」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處連接及本聲明。原文連接:https://blog.csdn.net/qq_34490018/article/details/82110578