字符串内存内部
這里要理解的要點(diǎn)是String Java對(duì)象與其內(nèi)容– private value字段下的char[]之間的區(qū)別。 String基本上是char[]數(shù)組的包裝器,將其封裝并使其無法修改,因此String可以保持不變。 另外, String類還記住該數(shù)組的實(shí)際部分(請(qǐng)參閱下文)。 這一切都意味著您可以擁有兩個(gè)指向相同char[]不同String對(duì)象(相當(dāng)輕量)。
我會(huì)告訴你一些例子,連同hashCode()的每個(gè)String和hashCode()內(nèi)部的char[] value字段(我將其稱之為文本字符串從區(qū)分)。 最后,我將顯示javap -c -verbose輸出以及測(cè)試類的常量池。 請(qǐng)不要將類常量池與字符串文字池混淆。 它們并不完全相同。 另請(qǐng)參見了解常量池的javap輸出 。
先決條件
為了進(jìn)行測(cè)試,我創(chuàng)建了一個(gè)實(shí)用程序方法來破壞String封裝:
private int showInternalCharArrayHashCode(String s) {final Field value = String.class.getDeclaredField("value");value.setAccessible(true);return value.get(s).hashCode(); }它將打印char[] value hashCode() ,有效地幫助我們了解此特定String是否指向相同的char[]文本。
一個(gè)類中的兩個(gè)字符串文字
讓我們從最簡(jiǎn)單的示例開始。
Java代碼
String one = "abc"; String two = "abc";順便說一句,如果您只寫"ab" + "c" ,則Java編譯器將在編譯時(shí)執(zhí)行串聯(lián),并且生成的代碼將完全相同。 僅當(dāng)在編譯時(shí)知道所有字符串時(shí),此方法才有效。
類常量池
每個(gè)類都有自己的常量池 -常量值列表,如果它們?cè)谠创a中多次出現(xiàn),則可以重用。 它包括常見的字符串,數(shù)字,方法名稱等。 這是上面示例中常量池的內(nèi)容:
需要注意的重要事項(xiàng)是String常量對(duì)象( #2 )和字符串指向的Unicode編碼文本"abc" ( #38 )之間的區(qū)別。
字節(jié)碼
這是生成的字節(jié)碼。 請(qǐng)注意, one引用和two引用都分配有指向"abc"字符串的相同#2常量:
輸出量
對(duì)于每個(gè)示例,我將打印以下值:
這兩對(duì)相等并不奇怪:
one.value: 23583040 two.value: 23583040 one: 8918249 two: 8918249這意味著不僅兩個(gè)對(duì)象都指向相同的char[] (下面的相同文本),所以equals()測(cè)試將通過。 但更重要的是, one和two是完全相同的引用! 因此, one == two也是正確的。 顯然,如果one和two指向同一個(gè)對(duì)象,則one.value和two.value必須相等。
文字和new String() ?
Java代碼
現(xiàn)在,我們都在等待該示例–一個(gè)字符串文字和一個(gè)使用相同文字的新String 。 這將如何運(yùn)作?
在源代碼中兩次使用了"abc"常量這一事實(shí)應(yīng)該給您一些提示……
類常量池與上面相同。
字節(jié)碼
ldc #2; //String abc astore_1 //onenew #3; //class java/lang/String dup ldc #2; //String abc invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V astore_2 //two仔細(xì)地看! 第一個(gè)對(duì)象的創(chuàng)建方法與上面相同,不足為奇。 它只需要從常量池中常量引用已經(jīng)創(chuàng)建的String ( #2 )。 但是,第二個(gè)對(duì)象是通過常規(guī)構(gòu)造函數(shù)調(diào)用創(chuàng)建的。 但! 第一個(gè)String作為參數(shù)傳遞。 可以將其反編譯為:
String two = new String(one); 輸出量
輸出有點(diǎn)令人驚訝。 第二對(duì)表示對(duì)String對(duì)象的引用是可以理解的-我們創(chuàng)建了兩個(gè)String對(duì)象-一個(gè)在常量池中為我們創(chuàng)建,第二個(gè)是為two手動(dòng)創(chuàng)建的。 但是,為什么第一對(duì)建議兩個(gè)String對(duì)象都指向同一個(gè)char[] value數(shù)組呢?
當(dāng)您查看String(String)構(gòu)造函數(shù)的工作原理時(shí),這一點(diǎn)變得很清楚(此處已大大簡(jiǎn)化):
public String(String original) {this.offset = original.offset;this.count = original.count;this.value = original.value; } 看到? 在基于現(xiàn)有對(duì)象創(chuàng)建新的String對(duì)象時(shí),它會(huì)重用 char[] value 。 String是不可變的,不需要復(fù)制已知永遠(yuǎn)不會(huì)修改的數(shù)據(jù)結(jié)構(gòu)。 而且,由于new String(someString)創(chuàng)建了現(xiàn)有字符串的精確副本,并且字符串是不可變的,因此顯然沒有理由同時(shí)存在兩者。
我認(rèn)為這是一些誤解的線索:即使您有兩個(gè)String對(duì)象,它們?nèi)钥赡苤赶蛳嗤膬?nèi)容。 如您所見, String對(duì)象本身很小。
運(yùn)行時(shí)修改和intern() ?
Java代碼
假設(shè)您最初使用了兩個(gè)不同的字符串,但是在進(jìn)行一些修改之后,它們都是相同的:
Java編譯器(至少是我的)不夠聰明,無法在編譯時(shí)執(zhí)行此類操作,請(qǐng)看一下:
類常量池
突然我們以指向兩個(gè)不同常量文本的兩個(gè)常量字符串結(jié)尾:
字節(jié)碼
ldc #2; //String abc astore_1 //oneldc #3; //String ?abc iconst_1 invokevirtual #4; //Method String.substring:(I)Ljava/lang/String; astore_2 //two拳頭弦照常構(gòu)造。 通過首先加載常量"?abc"字符串,然后在其上調(diào)用substring(1)來創(chuàng)建第二個(gè)。
輸出量
這里不足為奇–我們有兩個(gè)不同的字符串,指向內(nèi)存中兩個(gè)不同的char[]文本:
one.value: 27379847 two.value: 7615385 one: 8388097 two: 16585653 好吧,文本并沒有真正的不同 , equals()方法仍然會(huì)產(chǎn)生true 。 我們有兩個(gè)不必要的相同文本副本。
現(xiàn)在我們應(yīng)該進(jìn)行兩次練習(xí)。 首先,嘗試運(yùn)行:
在打印哈希碼之前。 one和two不僅指向同一文本,而且它們是相同的參考!
one.value: 11108810 two.value: 11108810 one: 15184449 two: 15184449 這意味著one.equals(two)和one == two測(cè)試都將通過。 我們還節(jié)省了一些內(nèi)存,因?yàn)?#34;abc"文本在內(nèi)存中僅出現(xiàn)一次(第二個(gè)副本將被垃圾回收)。
第二個(gè)練習(xí)略有不同,請(qǐng)查看以下內(nèi)容:
顯然one和two是兩個(gè)不同的對(duì)象,指向兩個(gè)不同的文本。 但是輸出如何表明它們都指向同一個(gè)char[]數(shù)組?!
one.value: 11108810 two.value: 8918249 one: 23583040 two: 23583040我將答案留給你。 它會(huì)教您substring()工作原理,這種方法的優(yōu)點(diǎn)是什么以及何時(shí)會(huì)導(dǎo)致大麻煩 。
得到教訓(xùn)
- String對(duì)象本身相當(dāng)便宜。 它指向的文本占用了大部分內(nèi)存
- String只是char[]的薄包裝,以保持不變性
- new String("abc")作為內(nèi)部文本表示被重用是不是真的那么貴。 但是還是要避免這樣的構(gòu)造。
- 從編譯時(shí)已知的常量值連接String時(shí),連接由編譯器而不是由JVM完成
- substring()有點(diǎn)棘手,但最重要的是,就使用的內(nèi)存和運(yùn)行時(shí)間而言,它都很便宜(在兩種情況下均保持不變)
參考:來自Java和社區(qū)博客的JCG合作伙伴 Tomasz Nurkiewicz的字符串內(nèi)存內(nèi)部結(jié)構(gòu) 。
翻譯自: https://www.javacodegeeks.com/2012/07/string-memory-internals.html
總結(jié)
- 上一篇: (linux flash)
- 下一篇: JAXB和根元素