String的内存模型,为什么String被设计成不可变的
String是Java中最常用的類(lèi),是不可變的(Immutable), 那么String是如何實(shí)現(xiàn)Immutable呢,String為什么要設(shè)計(jì)成不可變呢?
前言
關(guān)于String,收集一波基礎(chǔ),來(lái)源標(biāo)明最后,不確定是否權(quán)威, 希望有問(wèn)題可以得到糾正。
0. String的內(nèi)存模型
- Java8以及以后的字符串新建時(shí),直接在堆中生成對(duì)象,而字符創(chuàng)常量池位于Metaspace。必要的時(shí)候,會(huì)把堆中的指針存入Metaspace, 而不是復(fù)制。
- Metaspace位于虛擬機(jī)以外的直接內(nèi)存,因此大小和外部直接內(nèi)存有關(guān),但也可以通過(guò)指定參數(shù)設(shè)置-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
0.1 一些真實(shí)測(cè)試,以及某些推測(cè)
很難直接從百度出的中文資料中得到確切的答案,因?yàn)榇蠖嘁杂瀭饔?#xff0c;未經(jīng)驗(yàn)證。這里且做測(cè)試,先記住,因?yàn)楹懿磺樵缚泄俜轿臋n。
前期準(zhǔn)備
首先,要有字符串常量池的概念。然后知道String是怎么和常量池打交道的。這里的武器就是intern(),看一下javadoc:
/*** 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.* <p>* It follows that for any two strings {@code s} and {@code t},* {@code s.intern() == t.intern()} is {@code true}* if and only if {@code s.equals(t)} is {@code true}.* <p>* All literal strings and string-valued constant expressions are* interned. String literals are defined in section 3.10.5 of the* <cite>The Java™ Language Specification</cite>.** @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();即常量池存在,返回常量池中的那個(gè)對(duì)象,常量池不存在,則放入常量池,并返回本身。由此推斷兩個(gè)公式:
str.intern() == str //證明返回this本身,證明常量池不存在。 str.intern() != str //證明返回常量池中已存在的對(duì)象,不等于新建的對(duì)象。這兩個(gè)公式有什么用?
面試題雖然被很多牛人說(shuō)low(請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè)String實(shí)例”來(lái)面試了吧),但確實(shí)經(jīng)常出現(xiàn)new String以及幾個(gè)對(duì)象之類(lèi)的問(wèn)題。而這個(gè)問(wèn)題主要是考察String的內(nèi)存模型,連帶可以引出對(duì)Java中對(duì)象的內(nèi)存模型的理解。
通過(guò)判斷上述兩個(gè)公式,我們可以知道對(duì)象究竟是新建的,還是來(lái)自常量池,如此就可以坦然面對(duì)誰(shuí)等于誰(shuí)的問(wèn)題。
約定
- 為了準(zhǔn)確表達(dá),這里為偽地址表示指針位置,比如0xab表示"ab"這個(gè)對(duì)象的地址
- 測(cè)試基于jdk1.8.0_131.jdk
- 操作系統(tǒng): MacOS 10.12.6
- 內(nèi)存: 16G
- CPU: 2.2 GHz Intel Core i7
Java Visual VM
JDK提供一個(gè)可視化內(nèi)存查看工具jvisualvm。Mac由于安裝Java后已經(jīng)設(shè)置了環(huán)境變量,所以打開(kāi)命令行,直接輸入jvisualvm, 即可打開(kāi)。Windows下應(yīng)該是在bin目錄下找到對(duì)應(yīng)的exe文件,雙擊打開(kāi)。
OQL語(yǔ)言
在Java VisualVM中可以使用OQL來(lái)查找對(duì)象。具體可以查看Oracle博客。百度出來(lái)的結(jié)果都是摘抄的[深入理解Java虛擬機(jī)]這本書(shū)附錄里的內(nèi)容。但我表示用來(lái)使用行不通。一些用法不一樣。簡(jiǎn)單的歸納一些用的語(yǔ)法。
查詢一個(gè)內(nèi)容為RyanMiao的字符串:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"查詢前綴為Ryan的字符串:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"遍歷
filter(sort(map(heap.objects("java.lang.String"),function(heapString){if( ! counts[heapString.toString()]){counts[heapString.toString()] = 1;} else {counts[heapString.toString()] = counts[heapString.toString()] + 1;}return { string:heapString.toString(), count:counts[heapString.toString()]};}), 'lhs.count < rhs.count'),function(countObject) {if( countObject.string ){alreadyReturned[countObject.string] = true;return true;} else {return false;}});沒(méi)找到匹配前綴的做法,這里使用最笨的遍歷
filter( heap.objects("java.lang.String"), function(str){if(str != "Ryan" && str !="Miao" && str != "RyanMiao"){return false;}return true; } );0.1.1 通過(guò)=創(chuàng)建字符串
通過(guò)=號(hào)創(chuàng)建對(duì)象,運(yùn)行時(shí)只有一個(gè)對(duì)象存在。
/*** @author Ryan Miao* 等號(hào)賦值,注意字面量的存在*/ @Test public void testNewStr() throws InterruptedException {//str.intern(): 若常量池存在,返回常量池中的對(duì)象;若常量池不存在,放入常量池,并返回this。//=號(hào)賦值,若常量池存在,直接返回常量池中的對(duì)象0xs1,如果常量池不存在,則放入常量池,常量池中的對(duì)象也是0xs1String s1 = "RyanMiao";//0xs1Assert.assertTrue(s1.intern() == s1);//0xs1 == 0xs1 > trueThread.sleep(1000*60*60); }通過(guò)Java自帶的工具Java VisualVM來(lái)查詢內(nèi)存中的String實(shí)例,可以看出s1只有一個(gè)對(duì)象。操作方法如下。
為了動(dòng)態(tài)查看內(nèi)存,選擇休眠1h,run testNewStr(),然后打開(kāi)jvisualvm, 可以看到幾個(gè)vm列表,找到我們的vm,右鍵heamp dump.
然后,選擇右側(cè)的OQL,在查詢內(nèi)容編輯框里輸入:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"可以發(fā)現(xiàn),只有一個(gè)對(duì)象。
0.1.2 通過(guò)new創(chuàng)建字符串
通過(guò)new創(chuàng)建對(duì)象時(shí),參數(shù)RyanMiao作為字面量會(huì)生成一個(gè)對(duì)象,并存入字符創(chuàng)常量池。而后,new的時(shí)候又將創(chuàng)建另一個(gè)String對(duì)象,所以,最好不要采用這種方式使用String, 不然就是雙倍消耗內(nèi)存。
/*** @author Ryan Miao** 暴露的字面量(literal)也會(huì)生成對(duì)象,放入Metaspace*/ @Test public void testNew(){//new賦值,直接堆中創(chuàng)建0xs2, 常量池中All literal strings and string-valued constant expressions are interned,// "RyanMiao"本身就是一個(gè)字符串,并放入常量池,故intern()返回0xabString s2 = new String("RyanMiao");Assert.assertFalse(s2.intern() == s2);//0xRyanMiao == 0xs2 > false }0.1.3 通過(guò)拼接創(chuàng)造字符串
當(dāng)字符創(chuàng)常量池不存在此對(duì)象的的時(shí)候,返回本身。
/*** @author Ryan Miao* 上栗中,由于字面量(literal)會(huì)生成對(duì)象,并放入常量池,因此可以直接從常量池中取出(前提是此行代碼運(yùn)行之前沒(méi)有其他代碼運(yùn)行,常量池是干凈的)** 本次,測(cè)試非暴露字面量的str*/ @Test public void testConcat(){//沒(méi)有任何字面量為"RyanMiao"暴露給編譯器,所以常量池沒(méi)有創(chuàng)建"RyanMiao",所以,intern返回thisString s3 = new StringBuilder("Ryan").append("Miao").toString();Assert.assertTrue(s3.intern() == s3); }在Java Visual VM中,查詢以"Ryan"開(kāi)頭的變量:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"但,根據(jù)以上幾個(gè)例子,可以明顯看出來(lái),字符串字面量(literal)都是對(duì)象,于是上栗中應(yīng)該有三個(gè)對(duì)象:Ryan,Miao,RyanMiao。驗(yàn)證如下:
此時(shí)的內(nèi)存模型:
0.1.4 針對(duì)常量池中已存在的字符串
/*** @author Ryan Miao* 上栗中,只要不暴露我們最終的字符串,常量池基本不會(huì)存在,則每次新建(new)的時(shí)候,都會(huì)放入常量池,intern并返回本身。即常量池的對(duì)象即新建的對(duì)象本身。** 本次,測(cè)試某些常量池已存在的字符串*/ @Test public void testExist(){//為毛常量池存在java這個(gè)單詞//s4 == 0xs4, intern發(fā)現(xiàn)常量池存在,返回0xexistjavaString s4 = new StringBuilder("ja").append("va").toString();Assert.assertFalse(s4.intern() == s4); //0xexistjava == 0xs4 > false//int也一開(kāi)始就存在于常量池中了, intern返回0xexistintString s5 = new StringBuilder().append("in").append("t").toString();Assert.assertFalse(s5.intern()==s5); // 0xexistint == 0xs5 > false//由于字面量"abc"加載時(shí),已放入常量池,故s6 intern返回0xexistabc, 而s6是新建的0xs6String a = "abc";String s6 = new StringBuilder().append("ab").append("c").toString();Assert.assertFalse(s6.intern() == s6); //0xexistabc == 0xs6 > false}驗(yàn)證如下:
使用命令行工具javap -c TestString可以反編譯class,看到指令執(zhí)行的過(guò)程。
% javap -c TestString Warning: Binary file TestString contains com.test.java.string.TestString Compiled from "TestString.java" public class com.test.java.string.TestString {public com.test.java.string.TestString();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic void testNewStr() throws java.lang.InterruptedException;Code:0: ldc #2 // String RyanMiao2: astore_13: aload_14: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;7: aload_18: if_acmpne 1511: iconst_112: goto 1615: iconst_016: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V19: returnpublic void testNew() throws java.lang.InterruptedException;Code:0: new #5 // class java/lang/String3: dup4: ldc #2 // String RyanMiao6: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V9: astore_110: aload_111: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;14: aload_115: if_acmpne 2218: iconst_119: goto 2322: iconst_023: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V26: returnpublic void testConcat() throws java.lang.InterruptedException;Code:0: new #8 // class java/lang/StringBuilder3: dup4: ldc #9 // String Ryan6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V9: ldc #11 // String Miao11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;17: astore_118: aload_119: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;22: aload_123: if_acmpne 3026: iconst_127: goto 3130: iconst_031: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V34: returnpublic void testExist() throws java.lang.InterruptedException;Code:0: new #8 // class java/lang/StringBuilder3: dup4: ldc #14 // String ja6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V9: ldc #15 // String va11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;17: astore_118: aload_119: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;22: aload_123: if_acmpne 3026: iconst_127: goto 3130: iconst_031: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V34: new #8 // class java/lang/StringBuilder37: dup38: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V41: ldc #17 // String in43: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;46: ldc #18 // String t48: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;51: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;54: astore_255: aload_256: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;59: aload_260: if_acmpne 6763: iconst_164: goto 6867: iconst_068: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V71: ldc #19 // String abc73: astore_374: new #8 // class java/lang/StringBuilder77: dup78: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V81: ldc #20 // String ab83: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;86: ldc #21 // String c88: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;91: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;94: astore 496: aload 498: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;101: aload 4103: if_acmpne 110106: iconst_1107: goto 111110: iconst_0111: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V114: ldc2_w #22 // long 3600000l117: invokestatic #24 // Method java/lang/Thread.sleep:(J)V120: return }Java在compile的時(shí)候優(yōu)化了執(zhí)行邏輯
我以為使用了StringBuilder可以減少性能損耗啊,然而,編譯后的文件直接說(shuō)no,直接給替換成拼接了:
1. String是如何實(shí)現(xiàn)Immutable的?
Immutable是指String的對(duì)象實(shí)例生成后就不可以改變。相反,加入一個(gè)user類(lèi),你可以修改name,那么就不叫做Immutable。所以,String的內(nèi)部屬性必須是不可修改的。
1.1 私有成員變量
String的內(nèi)部很簡(jiǎn)單,有兩個(gè)私有成員變量:
/** The value is used for character storage. */ private final char value[];/** Cache the hash code for the string */ private int hash; // Default to 0而后并沒(méi)有對(duì)外提供可以修改這兩個(gè)屬性的方法,沒(méi)有set,沒(méi)有build。
1.2 Public的方法都是復(fù)制一份數(shù)據(jù)
String有很多public方法,要想維護(hù)這么多方法下的不可變需要付出代價(jià)。每次都將創(chuàng)建新的String對(duì)象。比如,這里講一個(gè)很有迷惑性的concat方法:
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); }從方法名上看,是拼接字符串。這樣下意識(shí)以為是原對(duì)象修改了內(nèi)容,所以對(duì)于str2 = str.concat("abc"),會(huì)認(rèn)為是str2==str。然后熟記String不可變定律的你肯定會(huì)反對(duì)。確實(shí)不是原對(duì)象,確實(shí)new了新String。同樣的道理,在其他String的public方法里,都將new一個(gè)新的String。因此就保證了原對(duì)象的不可變。說(shuō)到這里,下面的結(jié)果是什么?
String str2 = str.concat(""); Assert.assertFalse(str2 == str);按照String不可變的特性來(lái)理解,這里str2應(yīng)該是生成的新對(duì)象,那么肯定不等于str.所以是對(duì)的,是false。面試考這種題目也是醉了,為了考驗(yàn)大家對(duì)String API的熟悉程度嗎?看源碼才知道,當(dāng)拼接的內(nèi)容為空的時(shí)候直接返回原對(duì)象。因此,str2==str是true。
1.3 String是final的
由于String被聲明式final的,則我們不可以繼承String,因此就不能通過(guò)繼承來(lái)復(fù)寫(xiě)一些關(guān)于hashcode和value的方法。
2. String為什么要設(shè)計(jì)成Immutable?
一下內(nèi)容來(lái)自http://www.kogonuso.com/2015/03/why-string-is-immutable-or-final-class.html#sthash.VgLU1mDY.dpuf. 發(fā)現(xiàn)百度的中文版本基本也是此文的翻譯版。
緩存的需要
String是不可變的。因?yàn)镾tring會(huì)被String pool緩存。因?yàn)榫彺鍿tring字面量要在多個(gè)線程之間共享,一個(gè)客戶端的行為會(huì)影響其他所有的客戶端,所以會(huì)產(chǎn)生風(fēng)險(xiǎn)。如果其中一個(gè)客戶端修改了內(nèi)容"Test"為“TEST”, 其他客戶端也會(huì)得到這個(gè)結(jié)果,但顯然并想要這個(gè)結(jié)果。因?yàn)榫彺孀址畬?duì)性能來(lái)說(shuō)至關(guān)重要,因此為了移除這種風(fēng)險(xiǎn),String被設(shè)計(jì)成Immutable。
HashMap的需要
HashMap在Java里太重要了,而它的key通常是String類(lèi)型的。如果String是mutable,那么修改屬性后,其hashcode也將改變。這樣導(dǎo)致在HashMap中找不到原來(lái)的value。
多線程中需要
string的subString方法如下:
public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }如果String是可變的,即修改String的內(nèi)容后,地址不變。那么當(dāng)多個(gè)線程同時(shí)修改的時(shí)候,value的length是不確定的,造成不安全因素,無(wú)法得到正確的截取結(jié)果。而為了保證順序正確,需要加synchronzied,但這會(huì)得到難以想象的性能問(wèn)題。
保證hashcode
這和上條中HashMap的需要一樣,不可變的好處就是hashcode不會(huì)變,可以緩存而不用計(jì)算。
classloader中需要
The absolutely most important reason that String is immutable is that it is used by the class loading mechanism, and thus have profound and fundamental security aspects. Had String been mutable, a request to load "java.io.Writer" could have been changed to load "mil.vogoon.DiskErasingWriter"
String會(huì)在加載class的時(shí)候需要,如果String可變,那么可能會(huì)修改加載中的類(lèi)。
總之,安全性和String字符串常量池緩存是String被設(shè)計(jì)成不可變的主要原因。
參考
- https://stackoverflow.com/questions/3052442/what-is-the-difference-between-text-and-new-stringtext/3052456
- http://www.kogonuso.com/2015/03/why-string-is-immutable-or-final-class.html#sthash.VgLU1mDY.dpuf
- http://rednaxelafx.iteye.com/blog/774673
- http://www.jianshu.com/p/4ee6aec39c89?from=groupmessage
- http://www.cnblogs.com/yulei126/p/6777323.html
- https://blogs.oracle.com/sundararajan/querying-java-heap-with-oql
轉(zhuǎn)載于:https://www.cnblogs.com/woshimrf/p/why-string-is-immutable.html
總結(jié)
以上是生活随笔為你收集整理的String的内存模型,为什么String被设计成不可变的的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 建立自己的git账户并保存资料的重要性
- 下一篇: 2017 ACM/ICPC Asia R