摘要:證明返回常量池中已存在的對象,不等于新建的對象。為什么要設(shè)計成一下內(nèi)容來自發(fā)現(xiàn)百度的中文版本基本也是此文的翻譯版。總之,安全性和字符串常量池緩存是被設(shè)計成不可變的主要原因。
String是Java中最常用的類,是不可變的(Immutable), 那么String是如何實現(xiàn)Immutable呢,String為什么要設(shè)計成不可變呢?
前言關(guān)于String,收集一波基礎(chǔ),來源標(biāo)明最后,不確定是否權(quán)威, 希望有問題可以得到糾正。
0. String的內(nèi)存模型Java8以及以后的字符串新建時,直接在堆中生成對象,而字符創(chuàng)常量池位于Metaspace。必要的時候,會把堆中的指針存入Metaspace, 而不是復(fù)制。
Metaspace位于虛擬機以外的直接內(nèi)存,因此大小和外部直接內(nèi)存有關(guān),但也可以通過指定參數(shù)設(shè)置-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
0.1 一些真實測試,以及某些推測很難直接從百度出的中文資料中得到確切的答案,因為大多以訛傳訛,未經(jīng)驗證。這里且做測試,先記住,因為很不情愿啃官方文檔。
前期準(zhǔn)備首先,要有字符串常量池的概念。然后知道String是怎么和常量池打交道的。這里的武器就是intern(),看一下javadoc:
/** * Returns a canonical representation for the string object. ** A pool of strings, initially empty, is maintained privately by the * class {@code String}. *
* 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. *
* 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}. *
* All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * The Java™ Language Specification. * * @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();
即常量池存在,返回常量池中的那個對象,常量池不存在,則放入常量池,并返回本身。由此推斷兩個公式:
str.intern() == str //證明返回this本身,證明常量池不存在。 str.intern() != str //證明返回常量池中已存在的對象,不等于新建的對象。這兩個公式有什么用?
面試題雖然被很多牛人說low(請別再拿“String s = new String("xyz");創(chuàng)建了多少個String實例”來面試了吧),但確實經(jīng)常出現(xiàn)new String以及幾個對象之類的問題。而這個問題主要是考察String的內(nèi)存模型,連帶可以引出對Java中對象的內(nèi)存模型的理解。
通過判斷上述兩個公式,我們可以知道對象究竟是新建的,還是來自常量池,如此就可以坦然面對誰等于誰的問題。
約定為了準(zhǔn)確表達,這里為偽地址表示指針位置,比如0xab表示"ab"這個對象的地址
測試基于jdk1.8.0_131.jdk
操作系統(tǒng): MacOS 10.12.6
內(nèi)存: 16G
CPU: 2.2 GHz Intel Core i7
Java Visual VMJDK提供一個可視化內(nèi)存查看工具jvisualvm。Mac由于安裝Java后已經(jīng)設(shè)置了環(huán)境變量,所以打開命令行,直接輸入jvisualvm, 即可打開。Windows下應(yīng)該是在bin目錄下找到對應(yīng)的exe文件,雙擊打開。
OQL語言在Java VisualVM中可以使用OQL來查找對象。具體可以查看Oracle博客。百度出來的結(jié)果都是摘抄的[深入理解Java虛擬機]這本書附錄里的內(nèi)容。但我表示用來使用行不通。一些用法不一樣。簡單的歸納一些用的語法。
查詢一個內(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; } } );
沒找到匹配前綴的做法,這里使用最笨的遍歷
filter( heap.objects("java.lang.String"), function(str){ if(str != "Ryan" && str !="Miao" && str != "RyanMiao"){ return false; } return true; } );0.1.1 通過=創(chuàng)建字符串
通過=號創(chuàng)建對象,運行時只有一個對象存在。
/** * @author Ryan Miao * 等號賦值,注意字面量的存在 */ @Test public void testNewStr() throws InterruptedException { //str.intern(): 若常量池存在,返回常量池中的對象;若常量池不存在,放入常量池,并返回this。 //=號賦值,若常量池存在,直接返回常量池中的對象0xs1,如果常量池不存在,則放入常量池,常量池中的對象也是0xs1 String s1 = "RyanMiao";//0xs1 Assert.assertTrue(s1.intern() == s1);//0xs1 == 0xs1 > true Thread.sleep(1000*60*60); }
通過Java自帶的工具Java VisualVM來查詢內(nèi)存中的String實例,可以看出s1只有一個對象。操作方法如下。
為了動態(tài)查看內(nèi)存,選擇休眠1h,run testNewStr(),然后打開jvisualvm, 可以看到幾個vm列表,找到我們的vm,右鍵heamp dump.
然后,選擇右側(cè)的OQL,在查詢內(nèi)容編輯框里輸入:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"
可以發(fā)現(xiàn),只有一個對象。
通過new創(chuàng)建對象時,參數(shù)RyanMiao作為字面量會生成一個對象,并存入字符創(chuàng)常量池。而后,new的時候又將創(chuàng)建另一個String對象,所以,最好不要采用這種方式使用String, 不然就是雙倍消耗內(nèi)存。
/** * @author Ryan Miao * * 暴露的字面量(literal)也會生成對象,放入Metaspace */ @Test public void testNew(){ //new賦值,直接堆中創(chuàng)建0xs2, 常量池中All literal strings and string-valued constant expressions are interned, // "RyanMiao"本身就是一個字符串,并放入常量池,故intern()返回0xab String s2 = new String("RyanMiao"); Assert.assertFalse(s2.intern() == s2);//0xRyanMiao == 0xs2 > false }0.1.3 通過拼接創(chuàng)造字符串
當(dāng)字符創(chuàng)常量池不存在此對象的的時候,返回本身。
/** * @author Ryan Miao * 上栗中,由于字面量(literal)會生成對象,并放入常量池,因此可以直接從常量池中取出(前提是此行代碼運行之前沒有其他代碼運行,常量池是干凈的) * * 本次,測試非暴露字面量的str */ @Test public void testConcat(){ //沒有任何字面量為"RyanMiao"暴露給編譯器,所以常量池沒有創(chuàng)建"RyanMiao",所以,intern返回this String s3 = new StringBuilder("Ryan").append("Miao").toString(); Assert.assertTrue(s3.intern() == s3); }
在Java Visual VM中,查詢以"Ryan"開頭的變量:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"
但,根據(jù)以上幾個例子,可以明顯看出來,字符串字面量(literal)都是對象,于是上栗中應(yīng)該有三個對象:Ryan,Miao,RyanMiao。驗證如下:
此時的內(nèi)存模型:
/** * @author Ryan Miao * 上栗中,只要不暴露我們最終的字符串,常量池基本不會存在,則每次新建(new)的時候,都會放入常量池,intern并返回本身。即常量池的對象即新建的對象本身。 * * 本次,測試某些常量池已存在的字符串 */ @Test public void testExist(){ //為毛常量池存在java這個單詞 //s4 == 0xs4, intern發(fā)現(xiàn)常量池存在,返回0xexistjava String s4 = new StringBuilder("ja").append("va").toString(); Assert.assertFalse(s4.intern() == s4); //0xexistjava == 0xs4 > false //int也一開始就存在于常量池中了, intern返回0xexistint String s5 = new StringBuilder().append("in").append("t").toString(); Assert.assertFalse(s5.intern()==s5); // 0xexistint == 0xs5 > false //由于字面量"abc"加載時,已放入常量池,故s6 intern返回0xexistabc, 而s6是新建的0xs6 String a = "abc"; String s6 = new StringBuilder().append("ab").append("c").toString(); Assert.assertFalse(s6.intern() == s6); //0xexistabc == 0xs6 > false }
驗證如下:
使用命令行工具javap -c TestString可以反編譯class,看到指令執(zhí)行的過程。
% 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_0 1: invokespecial #1 // Method java/lang/Object."Java在compile的時候優(yōu)化了執(zhí)行邏輯":()V 4: return public void testNewStr() throws java.lang.InterruptedException; Code: 0: ldc #2 // String RyanMiao 2: astore_1 3: aload_1 4: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 7: aload_1 8: if_acmpne 15 11: iconst_1 12: goto 16 15: iconst_0 16: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V 19: return public void testNew() throws java.lang.InterruptedException; Code: 0: new #5 // class java/lang/String 3: dup 4: ldc #2 // String RyanMiao 6: invokespecial #6 // Method java/lang/String." ":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 14: aload_1 15: if_acmpne 22 18: iconst_1 19: goto 23 22: iconst_0 23: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 26: return public void testConcat() throws java.lang.InterruptedException; Code: 0: new #8 // class java/lang/StringBuilder 3: dup 4: ldc #9 // String Ryan 6: invokespecial #10 // Method java/lang/StringBuilder." ":(Ljava/lang/String;)V 9: ldc #11 // String Miao 11: 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_1 18: aload_1 19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 22: aload_1 23: if_acmpne 30 26: iconst_1 27: goto 31 30: iconst_0 31: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V 34: return public void testExist() throws java.lang.InterruptedException; Code: 0: new #8 // class java/lang/StringBuilder 3: dup 4: ldc #14 // String ja 6: invokespecial #10 // Method java/lang/StringBuilder." ":(Ljava/lang/String;)V 9: ldc #15 // String va 11: 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_1 18: aload_1 19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 22: aload_1 23: if_acmpne 30 26: iconst_1 27: goto 31 30: iconst_0 31: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 34: new #8 // class java/lang/StringBuilder 37: dup 38: invokespecial #16 // Method java/lang/StringBuilder." ":()V 41: ldc #17 // String in 43: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46: ldc #18 // String t 48: 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_2 55: aload_2 56: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 59: aload_2 60: if_acmpne 67 63: iconst_1 64: goto 68 67: iconst_0 68: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 71: ldc #19 // String abc 73: astore_3 74: new #8 // class java/lang/StringBuilder 77: dup 78: invokespecial #16 // Method java/lang/StringBuilder." ":()V 81: ldc #20 // String ab 83: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 86: ldc #21 // String c 88: 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 4 96: aload 4 98: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 101: aload 4 103: if_acmpne 110 106: iconst_1 107: goto 111 110: iconst_0 111: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 114: ldc2_w #22 // long 3600000l 117: invokestatic #24 // Method java/lang/Thread.sleep:(J)V 120: return }
我以為使用了StringBuilder可以減少性能損耗啊,然而,編譯后的文件直接說no,直接給替換成拼接了:
Immutable是指String的對象實例生成后就不可以改變。相反,加入一個user類,你可以修改name,那么就不叫做Immutable。所以,String的內(nèi)部屬性必須是不可修改的。
1.1 私有成員變量String的內(nèi)部很簡單,有兩個私有成員變量:
/** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0
而后并沒有對外提供可以修改這兩個屬性的方法,沒有set,沒有build。
1.2 Public的方法都是復(fù)制一份數(shù)據(jù)String有很多public方法,要想維護這么多方法下的不可變需要付出代價。每次都將創(chuàng)建新的String對象。比如,這里講一個很有迷惑性的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); }
從方法名上看,是拼接字符串。這樣下意識以為是原對象修改了內(nèi)容,所以對于str2 = str.concat("abc"),會認(rèn)為是str2==str。然后熟記String不可變定律的你肯定會反對。確實不是原對象,確實new了新String。同樣的道理,在其他String的public方法里,都將new一個新的String。因此就保證了原對象的不可變。說到這里,下面的結(jié)果是什么?
String str2 = str.concat(""); Assert.assertFalse(str2 == str);
按照String不可變的特性來理解,這里str2應(yīng)該是生成的新對象,那么肯定不等于str.所以是對的,是false。面試考這種題目也是醉了,為了考驗大家對String API的熟悉程度嗎?看源碼才知道,當(dāng)拼接的內(nèi)容為空的時候直接返回原對象。因此,str2==str是true。
1.3 String是final的由于String被聲明式final的,則我們不可以繼承String,因此就不能通過繼承來復(fù)寫一些關(guān)于hashcode和value的方法。
2. String為什么要設(shè)計成Immutable?一下內(nèi)容來自http://www.kogonuso.com/2015/... 發(fā)現(xiàn)百度的中文版本基本也是此文的翻譯版。
緩存的需要String是不可變的。因為String會被String pool緩存。因為緩存String字面量要在多個線程之間共享,一個客戶端的行為會影響其他所有的客戶端,所以會產(chǎn)生風(fēng)險。如果其中一個客戶端修改了內(nèi)容"Test"為“TEST”, 其他客戶端也會得到這個結(jié)果,但顯然并想要這個結(jié)果。因為緩存字符串對性能來說至關(guān)重要,因此為了移除這種風(fēng)險,String被設(shè)計成Immutable。
HashMap的需要HashMap在Java里太重要了,而它的key通常是String類型的。如果String是mutable,那么修改屬性后,其hashcode也將改變。這樣導(dǎo)致在HashMap中找不到原來的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)多個線程同時修改的時候,value的length是不確定的,造成不安全因素,無法得到正確的截取結(jié)果。而為了保證順序正確,需要加synchronzied,但這會得到難以想象的性能問題。
保證hashcode這和上條中HashMap的需要一樣,不可變的好處就是hashcode不會變,可以緩存而不用計算。
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會在加載class的時候需要,如果String可變,那么可能會修改加載中的類。
總之,安全性和String字符串常量池緩存是String被設(shè)計成不可變的主要原因。
參考https://stackoverflow.com/que...
http://www.kogonuso.com/2015/...
http://rednaxelafx.iteye.com/...
http://www.jianshu.com/p/4ee6...
http://www.cnblogs.com/yulei1...
https://blogs.oracle.com/sund...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/67644.html
摘要:性能,大量運用在哈希的處理中,由于的不可變性,可以只計算一次哈希值,然后緩存在內(nèi)部,后續(xù)直接取就好了。這是目前的一個底層字節(jié)碼的實現(xiàn),那么是不是沒有使用或者的必要了呢。 凱倫說,公眾號ID: KailunTalk,努力寫出最優(yōu)質(zhì)的技術(shù)文章,歡迎關(guān)注探討。 1. 前言 最近看到幾個有趣的關(guān)于Java核心類String的問題。 String類是如何實現(xiàn)其不可變的特性的,設(shè)計成不可變的好處...
摘要:性能當(dāng)字符串是不可變時,字符串常量池才有意義。字符串常量池的出現(xiàn),可以減少創(chuàng)建相同字面量的字符串,讓不同的引用指向池中同一個字符串,為運行時節(jié)約很多的堆內(nèi)存。 在學(xué)習(xí)Java的過程中,我們會被告知 String 被設(shè)計成不可變的類型。為什么 String 會被 Java 開發(fā)者有如此特殊的對待?他們的設(shè)計意圖和設(shè)計理念到底是什么?因此,我?guī)е韵氯齻€問題,對 String 進行剖析: ...
摘要:而用關(guān)鍵字調(diào)用構(gòu)造器,總是會創(chuàng)建一個新的對象,無論內(nèi)容是否相同。中對象的哈希碼被頻繁地使用比如在等容器中。字符串不變性保證了碼的唯一性因此可以放心地進行緩存。對于所有包含方式新建對象包括的連接表達式,它所產(chǎn)生的新對象都不會被加入字符串池中。 前言 前陣子和同事在吃飯時聊起Java的String,覺得自己之前的筆記寫的略顯零散。故此又重新整理了一下。 String在Java中算是一個有意...
摘要:原文出自本文總結(jié)了程序員常犯的個錯誤。可以看看為什么在中被設(shè)計成不可變父類和子類的構(gòu)造函數(shù)以上這段代碼出現(xiàn)編譯錯誤,因為默認(rèn)的父類構(gòu)造函數(shù)未定義。如果程序員定義構(gòu)造函數(shù),編譯器將不插入默認(rèn)的無參數(shù)構(gòu)造函數(shù)。 原文出自:http://www.programcreek.com/2014/05/top-10-mistakes-java-developers-make/ 本文總結(jié)了J...
閱讀 649·2021-11-11 16:55
閱讀 2160·2021-11-11 16:55
閱讀 1951·2021-11-11 16:55
閱讀 2341·2021-10-25 09:46
閱讀 1598·2021-09-22 15:20
閱讀 2270·2021-09-10 10:51
閱讀 1703·2021-08-25 09:38
閱讀 2613·2019-08-30 12:48