String:字符串,使用一對 「」 引發來表示html
String s1 = "mogublog" ; // 字面量的定義方式 String s2 = new String("moxi"); // new 對象的方式java
String聲明爲final的,不可被繼承 String實現了Serializable接口:表示字符串是支持序列化的。實現了Comparable接口:表示String能夠比較大小面試
string在jdk8及之前內部定義了final char[] value用於存儲字符串數據。JDK9時改成byte[]數組
String類的jdk8以前的實現將字符存儲在char數組中,每一個字符使用兩個字節(16位)。緩存
可是從許多不一樣的應用程序收集的數據代表,字符串是堆使用的主要組成部分,並且大多數字符串對象只包含拉丁字符。這些字符只須要一個字節的存儲空間,所以這些字符串對象的內部char數組中有一半的空間將不會使用。 例如 存ab 這個字符,若是使用char 數組,就要分配兩個字符的空間,即 四個字節, 可是ab 做爲英文,自己只需佔用兩個字節便可數據結構
以前 String 類使用 UTF-16 的 char[] 數組存儲,如今改成 byte[] 數組 外加一個編碼標誌位存儲,該編碼標誌將指定 String 類中 byte[] 數組的編碼方式app
結論:String不再用char[] 來存儲了,改爲了byte [] 加上編碼標記,節約了一些空間 ,jvm
同時基於String的數據結構,例如StringBuffer和StringBuilder也一樣作了修改ide
在Java語言中有8種基本數據類型和一種比較特殊的很是經常使用的類型String。這些類型爲了使它們在運行過程當中速度更快、更節省內存,都提供了一種常量池的概念。函數
常量池就相似一個Java系統級別提供的緩存。8種基本數據類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種。
代碼演示:
class Memory { public static void main(String[] args) {//line 1 int i = 1;//line 2 Object obj = new Object();//line 3 Memory mem = new Memory();//line 4 mem.foo(obj);//line 5 }//line 9 private void foo(Object param) {//line 6 String str = param.toString();//line 7 System.out.println(str); }//line 8 }複製代碼
示意圖:
如上圖所示,, 堆中的Object 對象在調用toString 方法後,將在String pool 中生成一個字符串對象,並返回給 頂層棧幀foo方法中的局部變量 str
String 內存分配的演進過程
JDK6 :
JDK7:
爲何要調整String 常量池的位置呢
字符串常量池是不會存儲相同內容的字符串的
字符串常量池中同一個字符串只存在一份
Java語言規範裏要求徹底相同的字符串字面量,應該包含一樣的Unicode字符序列(包含同一份碼點序列的常量),而且必須是指向同一個String類實例。
代碼示例:
public class StringTest4 { public static void main(String[] args) { System.out.println();//2330 System.out.println("1");//2331 個字符串 System.out.println("2"); System.out.println("3"); System.out.println("4"); System.out.println("5"); System.out.println("6"); System.out.println("7"); System.out.println("8"); System.out.println("9"); System.out.println("10");//2340 個字符串 //以下的字符串"1" 到 "10"不會再次加載 System.out.println("1");//2341 System.out.println("2");//2341 System.out.println("3"); System.out.println("4"); System.out.println("5"); System.out.println("6"); System.out.println("7"); System.out.println("8"); System.out.println("9"); System.out.println("10");//2341 } }複製代碼
在第一波開始,堆中的String 個數:
第一波結束時, 字符串個數,加了十個:
以後就再沒增長過了:
測試不一樣的 StringTable長度下,程序的性能
首先先建立一個擁有10W行不一樣字符的文件,程序自行編寫,這裏就不演示了
/** * -XX:StringTableSize=1009 */ public class StringTest2 { public static void main(String[] args) { BufferedReader br = null; try { br = new BufferedReader(new FileReader("words.txt")); long start = System.currentTimeMillis(); String data; while ((data = br.readLine()) != null) { //若是字符串常量池中沒有對應data的字符串的話,則在常量池中生成 data.intern(); } long end = System.currentTimeMillis(); System.out.println("花費的時間爲:" + (end - start));//1009:143ms 100009:47ms } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }複製代碼
代碼一:
public void test1() { String s1 = "a" + "b" + "c";//編譯期優化:等同於"abc" String s2 = "abc"; //"abc"必定是放在字符串常量池中,將此地址賦給s2 /* * 最終.java編譯成.class,再執行.class * String s1 = "abc"; * String s2 = "abc" */ System.out.println(s1 == s2); //true System.out.println(s1.equals(s2)); //true }複製代碼
結果打印的都是 true,無論是 地址值仍是 內容 都同樣,看下面解析出的字節碼指令,在指令0的位置從常量池中直接加載"abc"
0 ldc #2 <abc> 2 astore_1 3 ldc #2 <abc> 5 astore_2 6 getstatic #3 <java/lang/System.out> 9 aload_1 10 aload_2 11 if_acmpne 18 (+7) 14 iconst_1 15 goto 19 (+4) 18 iconst_0 19 invokevirtual #4 <java/io/PrintStream.println> 22 getstatic #3 <java/lang/System.out> 25 aload_1 26 aload_2 27 invokevirtual #5 <java/lang/String.equals> 30 invokevirtual #4 <java/io/PrintStream.println> 33 return複製代碼
代碼二:
public void test2(){ String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE" + "hadoop";//編譯期優化 //若是拼接符號的先後出現了變量,則至關於在堆空間中new String(),具體的內容爲拼接的結果:javaEEhadoop String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);//true //後面都是false 的緣由是,只要拼接時 有變量參與,都是至關於new 一個 不從常量池中共享 System.out.println(s3 == s5);//false System.out.println(s3 == s6);//false System.out.println(s3 == s7);//false System.out.println(s5 == s6);//false System.out.println(s5 == s7);//false System.out.println(s6 == s7);//false //intern():判斷字符串常量池中是否存在javaEEhadoop值,若是存在,則返回常量池中javaEEhadoop的地址; //若是字符串常量池中不存在javaEEhadoop,則在常量池中加載一份javaEEhadoop,並返回此對象的地址。 String s8 = s6.intern(); //s6 雖然是堆中new出來的,可是調用intern方法返回出的是 常量池中的,因此和 s3 地址同樣 System.out.println(s3 == s8);//true }複製代碼
前面說到, 一旦拼接操做有變量引用的參與,而不是所有都是字面量的形式, 就會 在堆中 新new一個對象,這是爲何呢?,下面用一個簡單的代碼解釋
代碼:
public void test3(){ String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2;//"ab" System.out.println(s3 == s4);//false }複製代碼
字節碼指令:
0 ldc #14 <a> //將字符a 從字符串常量池中獲取 2 astore_1 //放入 局部變量索引爲 1 的位置 (0爲this) 3 ldc #15 <b> //將字符b從字符串常量池中獲取 5 astore_2 //放入 局部變量索引爲 2 的位置 6 ldc #16 <ab> //將字符ab 從字符串常量池中獲取 8 astore_3 //放入 局部變量索引爲 3 的位置 9 new #9 <java/lang/StringBuilder> // new 一個 StringBuilder對象,開闢空間 12 dup 13 invokespecial #10 <java/lang/StringBuilder.<init>> //初始化該StringBuilder對象 16 aload_1 // 加載 局部變量表中索引爲1的值 ,也就是 a 17 invokevirtual #11 <java/lang/StringBuilder.append> //調用 StringBuilder對象 的 append 方法 20 aload_2 //加載 b 21 invokevirtual #11 <java/lang/StringBuilder.append> //一樣append 24 invokevirtual #12 <java/lang/StringBuilder.toString> //最後調用StringBuilder對象的toString方法 27 astore 4 //放入 局部變量表 索引爲 4的位置 29 getstatic #3 <java/lang/System.out> 32 aload_3 33 aload 4 35 if_acmpne 42 (+7) 38 iconst_1 39 goto 43 (+4) 42 iconst_0 43 invokevirtual #4 <java/io/PrintStream.println> 46 return複製代碼
從上面的 字節碼 逐行解釋中看, 帶有 變量的字符串拼接,其底層是使用 StringBuilder 對象
至關於以下代碼:
StringBuilder s = new StringBuilder(); s.append("a") s.append("b") s.toString() // 約等於 new String("ab"),方法內就是 new String ,可是有些不一樣 後面說複製代碼
補充:在jdk5.0以後使用的是StringBuilder,在jdk5.0以前使用的是StringBuffer
是否是全部字符串變量的拼接操做都是使用的是StringBuilder
那確定是否是的,看下面這種狀況:
public void test4(){ final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; System.out.println(s3 == s4);//true }複製代碼
字節碼: 看 指令9的位置,爲 s1+s2 的操做,並無使用 拼接的方式,而是在編譯器就已經處理好了
0 ldc #14 <a> 2 astore_1 3 ldc #15 <b> 5 astore_2 6 ldc #16 <ab> 8 astore_3 9 ldc #16 <ab> 11 astore 4 13 getstatic #3 <java/lang/System.out> 16 aload_3 17 aload 4 19 if_acmpne 26 (+7) 22 iconst_1 23 goto 27 (+4) 26 iconst_0 27 invokevirtual #4 <java/io/PrintStream.println> 30 return複製代碼
結論: 和直接使用字面量相同, 若是拼接字符變量都爲 final ,都是能夠在編譯器就能夠肯定結果的, 因此也被編譯器優化了(在寫代碼時,能夠寫final的均可以寫上,優化代碼)
代碼
public void test6(){ long start = System.currentTimeMillis(); //method1(100000);//4014 method2(100000);//7 long end = System.currentTimeMillis(); System.out.println("花費的時間爲:" + (end - start)); } public void method1(int highLevel){ String src = ""; for(int i = 0;i < highLevel;i++){ src = src + "a";//每次循環都會建立一個StringBuilder、String } } public void method2(int highLevel){ //只須要建立一個StringBuilder StringBuilder src = new StringBuilder(); for (int i = 0; i < highLevel; i++) { src.append("a"); } }複製代碼
咱們發現, 使用method1方法,拼接字符串10w次, 使用時間爲 4014ms,
而使用method2方法,append 方式,僅僅只需7ms, 差距如此之大
爲何?
對於上面的StringBuilder方式,還有更一步的改進方式
查看 StringBuilder 類底層的實現時,發現初始定義一個 char 型數組(JDK8),用於append操做, 在數組長度不夠時,會建立一個更長的,並進行拷貝, 這也有點浪費時間,因此在實際開發中,若是基本肯定要前先後後添加的字符串長度不高於某個限定值highLevel的狀況下,建議使用構造器實例化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]複製代碼
前面已經有使用過,而且大概解釋過,如今具體說明一下
public native String intern();複製代碼
intern是一個native方法,調用的是底層C的方法
字符串池最初是空的。在調用intern方法時,若是池中已經包含了由equals(object)方法肯定的與該字符串對象相等的字符串(也就是值相等),則返回池中的字符串。不然,該字符串對象值放一個到池中,並返回對該字符串對象的引用。
也就是說,若是在任意字符串上調用String.intern方法,那麼其返回結果所指向的那個類實例,必須和直接以字面量形式出現的字符串實例徹底相同。所以,下列表達式的值一定是true
("a"+"b"+"c").intern()=="abc"
通俗點講,String就是確保字符串在常量池裏有一份拷貝,這樣能夠節約內存空間,加快字符串操做任務的執行速度。
如何保證 變量s 指向的是字符串常量池中的數據呢?
方式一 : String s = "Hello"; //直接使用字面量
方式二: 使用 intern方法
這個面試題應該不少人都是知道的,答案是一個或者兩個, 下面用字節碼指令證實這個事
public class StringNewTest { public static void main(String[] args) { String str = new String("ab"); } }複製代碼
字節碼指令:
0 new #2 <java/lang/String> //堆中建立 String對象 3 dup 4 ldc #3 <ab> //從 常量池中獲取 ab 字符串對象,若是沒有 則會建立 6 invokespecial #4 <java/lang/String.<init>> //使用 ab 字面量去初始化 堆中的 String 對象 9 astore_1 10 return複製代碼
因此有上面的結論 ,一個或兩個, 若是常量池中沒有這個字面量的狀況是是會建立兩個的
看看 String類的帶參構造函數
private final char[] value; private int hash; //... public String(String var1) { this.value = var1.value; this.hash = var1.hash; }複製代碼
能夠看到,將常量池String對象中的value 屬性,也就是維護的char[] 和字符的hash值賦給了 new String 對象的 value屬性和 hash屬性,因此 new 出的String 對象的內容爲參數中的值, 這也說明了,雖然 常量池中的String對象 和new 出的 String 對象自己地址值不一樣,可是他們所維護的char[]倒是同一個
那麼new String(「a」) + new String(「b」) 會建立幾個對象?
public class StringNewTest { public static void main(String[] args) { String str = new String("a") + new String("b"); } }複製代碼
字節碼指令:
0 new #2 <java/lang/StringBuilder> // 1. StringBuilder 對象 3 dup 4 invokespecial #3 <java/lang/StringBuilder.<init>> 7 new #4 <java/lang/String> // 2. new String("a") 對象 10 dup 11 ldc #5 <a> //3. 常量池中 a 字符串對象 13 invokespecial #6 <java/lang/String.<init>> 16 invokevirtual #7 <java/lang/StringBuilder.append> 19 new #4 <java/lang/String> // 4. new String 對象 22 dup 23 ldc #8 <b> //5. 常量池中 b 字符串對象 25 invokespecial #6 <java/lang/String.<init>> 28 invokevirtual #7 <java/lang/StringBuilder.append> 31 invokevirtual #9 <java/lang/StringBuilder.toString> //toString 返回的 34 astore_1 35 return 複製代碼
全部上面的代碼 最多建立 6個對象
深刻剖析: StringBuilder的toString():
代碼:
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }複製代碼
這是StringBuilder 對象的toString 方法, 將 StringBuilder 類中append 處理的char[] 鏈接爲一個字符串,
和普通的 new String("ab")不同, 並無使用字面量的形式建立,因此沒有在 常量池中建立"ab" 字符串
因此這裏就建立了一個
在前面說到, intern() 方法是將判斷調用者字符串對象的值 是否在常量池中存在,若是存在 則返回常量池中的那個對象引用,若是不存在則 造一個, 可是這個造一個 ,在隨着JDK7 將字符串常量池移到堆空間時,發生了變化
代碼示例:
public class StringIntern { public static void main(String[] args) { String s = new String("1"); s.intern();//此方法調用前常量池中就已經有 字符串 "1"了 String s2 = "1"; System.out.println(s == s2); //jdk6:false jdk7/8:false // 執行完下一行代碼之後,字符串常量池中,是否存在"11"呢?答案:不存在!! String s3 = new String("1") + new String("1");//s3變量記錄的地址爲:new String("11") s3.intern(); String s4 = "11"; System.out.println(s3 == s4);// jdk6:false jdk7/8:true } }複製代碼
上面的代碼中
第一個輸出語句 不管如何都是打印false,那是由於在建立 new String("1")時已經在堆中建立了 字符串 "1",因此intern() 方法沒有再建立,而S2則引用了常量池中的對象,因此和 new String()的 s 一直都是false
第二個輸出語句 在jdk6 中打印了 false, 是由於 代碼 new String("1") + new String("1")至關於 建立了一個new String("11") 賦予了 s3 ,可是根據上一節說到的,並無在常量池中生成 "11",因此 當調用intern() 方法時, 建立了一個新的字符串 "11" 放到了常量池中,注意 此時的環境爲JDK6, 字符串常量池在 方法區中,和new的對象不在一塊兒,因此是建立一個新的方式
可是在JDK7 及之後的版本,倒是打印 true,是由於 字符串常量挪到了 堆中,和 new的對象在一塊兒,因此杜絕空間浪費, 調用intern() 方法建立對象時,沒有拷貝新的,而是存了 new String() 對象的引用到字符串常量池中,因此打印爲 true
內存示意圖:
JDK6:
JDK7
擴展
public class StringIntern1 { public static void main(String[] args) { String s3 = new String("1") + new String("1");//new String("11") //在字符串常量池中生成對象"11" String s4 = "11"; String s5 = s3.intern(); System.out.println(s3 == s4);//false System.out.println(s5 == s4);//true } }複製代碼
將上面的代碼中 聲明字面量提到調用intern() 方法先後,打印結果就固定了,那是由於在聲明"11" 時已經建立一個新的字符串到常量池中. 因此一定是false;
當調用 intern 方法時:
JDK1.6中,將這個字符串對象嘗試放入串池。
JDK1.7起,將這個字符串對象嘗試放入串池。
看下面的一段代碼:
public class StringIntern2 { static final int MAX_COUNT = 1000 * 10000; static final String[] arr = new String[MAX_COUNT]; public static void main(String[] args) { Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10}; long start = System.currentTimeMillis(); for (int i = 0; i < MAX_COUNT; i++) { // arr[i] = new String(String.valueOf(data[i % data.length])); arr[i] = new String(String.valueOf(data[i % data.length])).intern(); } long end = System.currentTimeMillis(); System.out.println("花費的時間爲:" + (end - start)); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }複製代碼
上面的代碼中,將 1-10 每次使用 String.valueOf 的方式轉換爲 字符串 存入數組中,循環1000W次, 下面看 使用intern方法和不使用的區別
不使用: valueOf 方法中每次都在堆中new 了一個新的字符串對象, 因此共建立了 1000w個對象
使用intern: 雖然每次也都建立了 String 對象, 可是最後返回的 倒是intern 方法返回的 字符串常量池中的,會被重用, 而由於數組中並無指向在堆中建立的String 對象,將在垃圾回收時 被銷燬,減小內存,查看內存數據也是如此
結論:
代碼演示: 建立10w個字符串
public class StringGCTest { public static void main(String[] args) { for (int j = 0; j < 100000; j++) { String.valueOf(j).intern(); } } }複製代碼
jvm 參數: -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails 打印 字符串常量池中的信息和垃圾回收的信息
打印信息以下, 很明顯在新生代PSYoungGen發生了垃圾回收的行爲, 而且看出 字符串常量池中也不足10w個對象
打印內容:
Heap PSYoungGen total 4608K, used 3883K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000) eden space 4096K, 82% used [0x00000000ffb00000,0x00000000ffe50fb0,0x00000000fff00000) from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000) to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) ParOldGen total 11264K, used 228K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000) object space 11264K, 2% used [0x00000000ff000000,0x00000000ff039010,0x00000000ffb00000) Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 14158 = 339792 bytes, avg 24.000 Number of literals : 14158 = 603200 bytes, avg 42.605 Total footprint : = 1103080 bytes Average bucket size : 0.708 Variance of bucket size : 0.711 Std. dev. of bucket size: 0.843 Maximum bucket size : 6 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 62943 = 1510632 bytes, avg 24.000 Number of literals : 62943 = 3584040 bytes, avg 56.941 Total footprint : = 5574776 bytes Average bucket size : 1.049 Variance of bucket size : 0.824 Std. dev. of bucket size: 0.908 Maximum bucket size : 5複製代碼
補充: G1中 String 去重操做
這個去重操做針對的不是 String自己,由於 String 常量池中 自己就不是重複的,而是針對 new String() 中維護的char[]
String 去重操做的背景
對許多Java應用(有大的也有小的)作的測試得出如下結果:
許多大規模的Java應用的瓶頸在於內存,測試代表,在這些類型的應用裏面,Java堆中存活的數據集合差很少25%是String對象。更進一步,這裏面差很少一半String對象是重複的,重複的意思是說:
str1.equals(str2)= true。堆上存在重複的String對象必然是一種內存的浪費。這個項目將在G1垃圾收集器中實現自動持續對重複的String對象進行去重,這樣就能避免浪費內存。
String 去重的的具體實現
命令行選項: