Java中String揭秘

String對象是我們?nèi)粘J褂玫膶ο箢愋?,字符串對象或者其等價(jià)對象(如char數(shù)組),在內(nèi)存中總是占據(jù)了最大的空間塊,因此如何高效地處理字符串,是提高系統(tǒng)整體性能的關(guān)鍵。
在此之前,String作為一個(gè)對象類型,我們必須清楚Java對象的創(chuàng)建以為對象的內(nèi)存結(jié)構(gòu)。
\color{blue}{1.對象的創(chuàng)建以及內(nèi)存結(jié)構(gòu) }
創(chuàng)建一個(gè)對象通常需要使用new關(guān)鍵字,當(dāng)虛擬機(jī)遇到一條new指令的時(shí)候,首先會檢查這個(gè)指令的參數(shù)是在常量池中定位到一個(gè)符號引用,并且檢查這個(gè)符號引用代表的類是否已經(jīng)被加載、解析和初始化。如果是則執(zhí)行相應(yīng)的類加載過程。

類加載檢查結(jié)束之后,虛擬機(jī)將為新生對象分配內(nèi)存,java中為對象分配內(nèi)存有兩種方式,一種是\color{red}{指針碰撞},該方法適用于內(nèi)存規(guī)整的情況,在中間放一個(gè)指針作為分界點(diǎn)的指示器,使用過的內(nèi)存和空閑的內(nèi)存各放在一邊,當(dāng)需要分配內(nèi)存的時(shí)候只需要將指針移動即可。另一種是\color{red}{空閑列表},如果java堆中的內(nèi)存不是規(guī)整的,虛擬機(jī)會維護(hù)一張列表,記錄哪塊內(nèi)存可用,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄。采用哪種分配方式是根據(jù)java堆是否規(guī)整決定的。而java堆是否規(guī)整由JVM是否使用帶有壓縮整理功能的垃圾收集器決定。

另外需要考慮的是內(nèi)存分配過程中線程安全的情況。有如下兩種解決方案;

  • 堆內(nèi)存分配的動作做同步處理。
  • 另一種是把內(nèi)存分配的動作按照線程劃分為不同的空間之中執(zhí)行,即每一個(gè)線程在java堆中預(yù)先分配一小塊內(nèi)存(TLAB),哪個(gè)線程需要分配內(nèi)存首先在TLAB上分配,如果TLAB分配完了之后,才會同步分配新的TLAB。JVM是否使用TLAB由參數(shù)\color{red}{-XX:/-UseTLAB}來決定。

內(nèi)存分配完畢之后想,虛擬機(jī)需要分配到的內(nèi)存空間初始化為零值。這一步操作保證了對象的實(shí)例字段在java代碼中可以不賦初始值就可以使用,接下來虛擬機(jī)要對對象進(jìn)行必要的設(shè)置,例如這個(gè)對象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息等,這些信息存放在對象的對象頭中。這些工作完成之后,從JVM的角度來看一個(gè)對象已經(jīng)創(chuàng)建成功了,從java的角度來看還需要執(zhí)行init方法,將對象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對象才算完全產(chǎn)生出來。

在HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲的布局可分為三個(gè)部分,即對象頭,實(shí)例數(shù)據(jù)和對齊填充。
對象頭包括兩個(gè)部分,第一部分用來存儲對象自身運(yùn)行時(shí)的數(shù)據(jù),如哈希碼,GC分代年齡、線程所持有的鎖等,官方稱為“Mark Word”。第二個(gè)部分為類型 指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè) 指針來確定這個(gè)對象屬于哪個(gè)類的實(shí)例。

實(shí)例數(shù)據(jù)是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內(nèi)容。

對齊填充并不是必須的,僅僅起到占位符的作用,HotSpot虛擬機(jī)需要對象起始地址必須是8字節(jié)的整數(shù)倍,對象部分正好是8字節(jié)的整數(shù)倍,所以當(dāng)實(shí)例數(shù)據(jù)部分沒有對齊時(shí),需要通過對齊填充來對齊。
\color{blue}{2.String揭秘}
對于String類型,我們首先來看看其JDK內(nèi)部的成員變量的聲明代碼:

sss.png

我們會看到它內(nèi)部維護(hù)著一個(gè)char數(shù)組,而且它是由final關(guān)鍵詞修飾的,說明它一旦創(chuàng)建之后不可變。對于String的創(chuàng)建,比較特殊一些,我們來看一下它的具體創(chuàng)建原理:

  • 不管使用任何方式來創(chuàng)建一個(gè)字符串S的時(shí)候,Java運(yùn)行時(shí)會拿著這個(gè)S字符串在String池中查找是否存在內(nèi)容相同的字符串對象,如果不存在,則在池中創(chuàng)建一個(gè)字符串S,否則不會創(chuàng)建對象,也不會在池中添加。
  • 前面提到使用new關(guān)鍵創(chuàng)建對象,那么肯定會在堆棧創(chuàng)建一個(gè)新的對象,String也是一樣的。
  • 使用直接指定或者使用純字符串拼接來創(chuàng)建String對象,則僅僅會檢查String池中的字符串,池中沒有就創(chuàng)建一個(gè),如果存在,就不需要?jiǎng)?chuàng)建新的,但是絕對不會在堆棧區(qū)再去創(chuàng)建對象。
  • 使用包含變量的表達(dá)式來創(chuàng)建String對象時(shí),則不僅會檢查并維護(hù)Sting池,而且還會在堆棧區(qū)創(chuàng)建一個(gè)新的String對象。

最常見的String操作莫過于拼接字符串了,在拼接字符串時(shí),我們盡量用+,因?yàn)橥ǔ>幾g器會做出優(yōu)化,如String test="hello "+"world",編譯器會將其視為String test="hello world"。所以在拼接國泰字符串時(shí),我們需要盡量使用StringBuffer或者StringBuilder的append方法,這樣可以減少構(gòu)造過多的臨時(shí)String對象。下面我們來看一個(gè)簡單的實(shí)例來證實(shí):

ddd.png

在String對象中有一個(gè)特殊的方法,它是一個(gè)本地方法,當(dāng)調(diào)用該方法時(shí),如果池中已經(jīng)包含了一個(gè)等于此String對象的字符串,則返回池中的字符串,否則,將此對象添加到池中,并且返回String對象的引用。

在上面的一個(gè)例子中,str1和str4并不是同一個(gè)對象引用,因此不相等,那么我們使用intern方法,添加一句,觀察運(yùn)行結(jié)果:


sdsds.png

也許很多人想到我們可以使用intern方法來創(chuàng)建對象,避免使用new創(chuàng)建大量的對象,但是這也有一個(gè)隱含的問題。

使用String的\color{red}{intern()}方法返回JVM對字符串緩存池里已經(jīng)存在的字符串引用,從而解決內(nèi)存性能問題,但是intern方法使用的池是JVM全局的池,很多情況下我們的程序并不需要如此大作用域的緩存,而且,它所使用的是JVM heap中PermGen對應(yīng)的區(qū)域,PermGen通常是用來存放裝載類和創(chuàng)建類實(shí)例時(shí)用到的元數(shù)據(jù),因此,使用過多的intern方法會導(dǎo)致PermGen過度增長而最后返回OOM,因此垃圾收集器不會對緩存的String做垃圾回收,因此不建議使用。

實(shí)際中,如果需要?jiǎng)?chuàng)建大量的字符串,我們可以自己構(gòu)建緩存,比如使用HashMap,將需緩存的String作為key和value放在HashMap中,例如下面代碼:

public String getCacheString(String key){
    String temp=cacheMap.get(key);
    if(temp!=null){
        return temp;
    }else{
        cacheMap.put(key,key);
        return key;
    }
}

在字符串的使用中,另一個(gè)常見的操作是截取字符串,在String內(nèi)部提供了\color{red}{substring}方法供我們使用,其源碼如下(1.8版本):

41.png

42.png
43.png

從上面的源碼可以看出,substring方法截取字符串的時(shí)候,會將String的原生內(nèi)容復(fù)制到新的子字符串中,從整個(gè)方法的調(diào)用鏈來看,它會保存原始String。因此這也引發(fā)了下面的問題。

  • 在一個(gè)大字符串中我們需要截取的字符串遠(yuǎn)遠(yuǎn)小于其原始字符串的長度時(shí),不建議直接使用substring方法截取后直接返回,這樣會造成內(nèi)存泄漏,我們可以使用new String的方式來創(chuàng)建一個(gè)個(gè)字符串對象,將垃圾回收交給JVM GC,避免內(nèi)存泄漏問題。
  • 當(dāng)在一個(gè)大字符串中我們需要截取的字符串幾乎和原始字符串長度相等的時(shí)候,我們可以放心的使用substring方法來截取返回。

所幸的是,在JDK1.7之后的版本中,將substring的內(nèi)部實(shí)現(xiàn)修改為使用Arrays進(jìn)行拷貝,不再復(fù)用之前的原字符串,因此使其得以回收,所以String內(nèi)存泄漏的問題也得到了修復(fù)。

如果使用了1.7之前的API,也可以使用下面的方法來解決內(nèi)存泄漏問題。

看一個(gè)用例:

public class TestSubString {

    public static void main(String[] args) {
        List<String> list=new ArrayList<String>();
        for(int i=0;i<1000;i++){
            SubString1 str1=new SubString1();
            SubString2 str2=new SubString2();
            list.add(str1.getSubString(1,6));
            list.add(str2.getSubString(1,6));
        }

    }

    public static class SubString1{
        public String str=new String(new char[10000000]);
        public String getSubString(int begin,int end){
            return new String(str.substring(begin, end));      //使用new重新創(chuàng)建字符串
        }
    }

    public static class SubString2{
        public String str=new String(new char[10000000]);
        public String getSubString(int begin,int end){
            return str.substring(begin, end);                   //直接截取返回
        }
    }
}

在這個(gè)用例中,原始字符串很大,但是需要截取的卻是很小的一段,因此在這種場景下推薦使用SubString1重新new一個(gè)字符串來釋放原始字符串的方式來截取字符串,這樣避免了原始字符串不能被回收,存在內(nèi)存泄漏的問題

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、基礎(chǔ)知識:1、JVM、JRE和JDK的區(qū)別:JVM(Java Virtual Machine):java虛擬機(jī)...
    殺小賊閱讀 2,565評論 0 4
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,658評論 1 32
  • 從網(wǎng)上復(fù)制的,看別人的比較全面,自己搬過來,方便以后查找。原鏈接:https://www.cnblogs.com/...
    lxtyp閱讀 1,436評論 0 9
  • 一、String 類 1、定義: 1、從概念上講,java字符串就是Unicode字符序列。每個(gè)用雙引號括起來的字...
    玉圣閱讀 1,744評論 0 1
  • 一 、java虛擬機(jī)底層結(jié)構(gòu)詳解 我們知道,一個(gè)JVM實(shí)例的行為不光是它自己的事,還涉及到它的子系統(tǒng)、存儲區(qū)域、...
    葡萄喃喃囈語閱讀 1,582評論 0 4

友情鏈接更多精彩內(nèi)容