1.String的基本特性
-
String:字元串,使用一對""引起來表示。
– String sl = “hello”;//字面量的定義方式
– String s2 = new String(“hello”) ;
- String類聲明為final的, 不可被繼承
- String實作了Serializable接口:表示字元串是支援序列化的。
- 實作了Comparable接口:表示String可以比較大小
- String在jdk8及以前内部定義了final char[],value用于存儲字元串資料。jdk9時改為byte[]
結論: String再也不用char[] 來存儲啦,改成了byte[] 加上編碼标記,節約了一些空間。StringBuffer和StringBuilder也做了一些修改
public final class String implements java.io.Serializable, Comparable<String>,CharSequence {
@Stable
private final byte[] value;
}
-
String:代表不可變的字元序列。簡稱:不可變性。
– 當對字元串重新指派時,需要重寫指定記憶體區域指派,不能使用原有的value進行指派。
– 當對現有的字元串進行連接配接操作時,也需要重新指定記憶體區域指派,不能使用原有的value進行指派。
– 當調用String的replace()方法修改指定字元或字元串時,也需要重新指定記憶體區域指派,不能使用原有的value進行指派。
- 通過字面量的方式(差別于new)給一個字元串指派,此時的字元串值聲明在字元串常量池中。
/**
* String的基本使用:展現String的不可變性
*/
public class StringTest1 {
@Test
public void test1() {
String s1 = "abc";//字面量定義的方式,"abc"存儲在字元串常量池中
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//判斷位址:true --> false
System.out.println(s1);//
System.out.println(s2);//abc
}
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);//abcdef
System.out.println(s1);//abc
}
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}
}
-
字元串常量池中是不會存儲相同内容的字元串的。
– String的String Pool 是一個固定大小的Hashtable,預設值大小長度是1009。如果放進StringPool的String非常多, 就會造成Hash沖突嚴重,進而導緻連結清單會很長,而連結清單長了後直接會造成的影響就是當調用String. intern時性能會大幅下降。
– 使用一XX: StringTableSize可設定StringTable的長度
– 在jdk6中StringTable是固定的,就是1009的長度,是以如果常量池中的字元串過多就會導緻效率下降很快。StringTableSize設 置沒有要求
– 在jdk7中,StringTable的長度預設值是60013
– jdk8開始,1009是StringTable長度可設定的最小值
2.String的記憶體配置設定
- 在Java語言中有8種基本資料類型和一種比較特殊的類型String。這些類型為了使它們在運作過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
- 常量池就類似一.個Java系統級别提供的緩存。8種基本資料類型的常量池都是系統協調的,String類 型的常量池比較特殊。它的主要使用方法有兩種。
-
直接使用雙引号聲明出來的String對象會直接存儲在常量池中。
–比如: String info = “abc” ;
– 如果不是用雙引号聲明的String對象,可以使用String提供的intern()方法。這個後面重點談
- Java 6及以前,字元串常量池存放在永久代。
-
Java 7中Oracle的工程師對字元串池的邏輯做了很大的改變,即将字元串常量池的位置調整到Java堆内。
– 所有的字元串都儲存在堆(Heap)中,和其他普通對象一樣,這樣可以讓你在進行調優應用時僅需要調整堆大小就可以了。
– 字元串常量池概念原本使用得比較多,但是這個改動使得我們有足夠的理由讓我們重新考慮在Java 7中使用String. intern()。
- Java8元空間,字元串常量在堆
StringTable為什麼要調整
①永久代permSize預設比較小;
②永久代的垃圾回收頻率低;

3.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
}
4.字元串拼接操作
- 1.常量與常量的拼接結果在常量池,原理是編譯期優化
@Test
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
}
- 2.常量池中不會存在相同内容的常量。
- 3.隻要其中有一個是變量,結果就在堆中。變量拼接的原理是StringBuilder
- 4.如果拼接的結果調用intern()方法,則主動将常量池中還沒有的字元串對象放入池中,并傳回此對象位址。
package com.wfg.stringtable;
import org.junit.Test;
/**
* java
*
* @Title: com.wfg.stringtable
* @Date: 2020/9/9 18:11
* @Author: wfg
* @Description:
* @Version:
*/
public class Demo3 {
@Test
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
}
@Test
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
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();
System.out.println(s3 == s8);//true
}
}
圖示
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的s1 + s2 的執行細節:(變量s是我臨時定義的)
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 約等于 new String("ab")
補充:在jdk5.0之後使用的是StringBuilder,
在jdk5.0之前使用的是StringBuffer
*/
String s4 = s1 + s2;//
System.out.println(s3 == s4);//false
}
/*
1. 字元串拼接操作不一定使用的是StringBuilder!
如果拼接符号左右兩邊都是**字元串常量或常量引用**,則仍然使用編譯期優化,即非StringBuilder的方式。
2. 針對于final修飾類、方法、基本資料類型、引用資料類型的量的結構時,能使用上final的時候建議使用上。
*/
@Test
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
//練習:
@Test
public void test5(){
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);//false
final String s4 = "javaEE";//s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);//true
}
4.1 拼接操作與append的效率對比
append效率要比字元串拼接高很多
體會執行效率:通過StringBuilder的append()的方式添加字元串的效率要遠高于使用String的字元串拼接方式!
詳情:① StringBuilder的append()的方式:自始至終中隻建立過一個StringBuilder的對象
使用String的字元串拼接方式:建立過多個StringBuilder和String的對象
② 使用String的字元串拼接方式:記憶體中由于建立了較多的StringBuilder和String的對象,記憶體占用更大;如果進行GC,需要花費額外的時間。
**改進的空間**:在實際開發中,如果基本确定要前前後後添加的字元串長度不高于某個限定值highLevel的情況下,建議使用構造器執行個體化:
StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
/*
體會執行效率:通過StringBuilder的append()的方式添加字元串的效率要遠高于使用String的字元串拼接方式!
詳情:① StringBuilder的append()的方式:自始至終中隻建立過一個StringBuilder的對象
使用String的字元串拼接方式:建立過多個StringBuilder和String的對象
② 使用String的字元串拼接方式:記憶體中由于建立了較多的StringBuilder和String的對象,記憶體占用更大;如果進行GC,需要花費額外的時間。
改進的空間:在實際開發中,如果基本确定要前前後後添加的字元串長度不高于某個限定值highLevel的情況下,建議使用構造器執行個體化:
StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
*/
@Test
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
}
// System.out.println(src);
}
public void method2(int highLevel){
//隻需要建立一個StringBuilder
StringBuilder src = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
// System.out.println(src);
}
5.intern()的使用
如果不是用雙引号聲明的String對象,可以使用String提供的intern方法: intern方法會從字元串常量池中查詢目前字元串是否存在,若不存在就會将目前字元串放入常量池中。
比如: String myInfo = new String(“I love u”).intern();
也就是說,如果在任意字元串上調用String. intern方法,那麼其傳回結果所指向的那個類執行個體,必須和直接以常量形式出現的字元串執行個體完全相同。是以,下 清單達式的值必定是true:
(“a” + “b” + “c”).intern()== “abc”;
通俗點講,Interned String就是確定字元串在記憶體裡隻有一份拷貝,這樣可以節約記憶體空間,加快字元串操作任務的執行速度。注意,這個值會被存放在字元串内部池(String Intern Pool)。
5.1 new String(“ab”)會建立幾個對象,new String(“a”)+new String(“b”)呢
public class StringNewTest {
public static void main(String[] args) {
String str = new String("ab");
//String str = new String("a") + new String("b");
}
}
-
new String(“ab”)會建立幾個對象?看位元組碼,就知道是兩個。
一個對象是:new關鍵字在堆空間建立的
另一個對象是:字元串常量池中的對象"ab"。 位元組碼指令:ldc
- new String(“a”) + new String(“b”)呢?
對象1:new StringBuilder()
對象2: new String(“a”)
對象3: 常量池中的"a"
對象4: new String(“b”)
對象5: 常量池中的"b"
深入剖析: StringBuilder的toString():
對象6 :new String(“ab”)
強調一下,toString()的調用,在字元串常量池中,沒有生成"ab"
StringBuilder 的toString方法
5.2 關于String.intern()的面試題
**
* 如何保證變量s指向的是字元串常量池中的資料呢?
* 有兩種方式:
* 方式一: String s = "shkstart";//字面量定義的方式
* 方式二: 調用intern()
* String s = new String("shkstart").intern();
* String s = new StringBuilder("shkstart").toString().intern();
*
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
String s1 = s.intern();//調用此方法之前,字元串常量池中已經存在了"1"
String s2 = "1";
//s 指向堆空間"1"的記憶體位址
//s1 指向字元串常量池中"1"的記憶體位址
//s2 指向字元串常量池已存在的"1"的記憶體位址 是以 s1==s2
System.out.println(s == s2);//jdk6:false jdk7/8:false
System.out.println(s1 == s2);//jdk6: true jdk7/8:true
System.out.println(System.identityHashCode(s));//21685669
System.out.println(System.identityHashCode(s1));//2133927002
System.out.println(System.identityHashCode(s2));//2133927002
//s3變量記錄的位址為:new String("11")
String s3 = new String("1") + new String("1");
//執行完上一行代碼以後,字元串常量池中,是否存在"11"呢?答案:不存在!!
//在字元串常量池中生成"11"。如何了解:jdk6:建立了一個新的對象"11",也就有新的位址。
// jdk7:此時常量中并沒有建立"11",而是建立一個指向堆空間中new String("11")的位址
s3.intern();
//s4變量記錄的位址:使用的是上一行代碼代碼執行時,在常量池中生成的"11"的位址
String s4 = "11";
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}
//在字元串常量池中生成"11"。如何了解:jdk6:建立了一個新的對象"11",也就有新的位址。
// jdk7:此時常量中并沒有建立"11",而是建立一個指向堆空間中new String(“11”)的位址
@Test
public void test3(){
String s1 = new String("a");
s1.intern();
String s2 = "a";
System.out.println(s1==s2); // false
String s3 = new String("b")+new String("c");
s3.intern();
String s4 = "bc";
System.out.println(s3==s4); //True
}
在JDK 1.7下,當執行s3.intern();時,因為常量池中沒有“bc”這個字元串,是以會在常量池中生成一個對堆中的“bc”的引用(注意這裡是引用 ,就是這個差別于JDK 1.6的地方。在JDK1.6下是生成原字元串的拷貝),而在進行String str1 = “bc”;字面量指派的時候,常量池中已經存在一個引用,是以直接傳回了該引用,是以s3和s4都指向堆中的同一個字元串,傳回true。
其實這個地方的我的最大疑惑就是:s1.intern() 中常量池中存的是"a"的拷貝,為啥 s3.intern()在常量池中存的就是bc的引用?
5.3 拓展
public class StringIntern1 {
public static void main(String[] args) {
//StringIntern.java中練習的拓展:
String s3 = new String("1") + new String("1");//new String("11")
//執行完上一行代碼以後,字元串常量池中,是否存在"11"呢?答案:不存在!!
String s4 = "11";//在字元串常量池中生成對象"11"
String s5 = s3.intern();
System.out.println(s3 == s4);//false
System.out.println(s5 == s4);//true
}
}
5.4 總結String的intern()的使用
jdk1.6中,将這個字元串對象嘗試放入串池。
➢如果字元串常量池中有,則并不會放入。傳回已有的串池中的對象的位址
➢如果沒有,會把此對象複制一份,放入串池,并傳回串池中的對象位址
Jdk1.7起,将這個字元串對象嘗試放入串池。
➢如果字元串常量池中有,則并不會放入。傳回已有的串池中的對象的位址
➢如果沒有,則會把對象的引用位址複制一份,放入串池,并傳回串池中的引用位址
5.5 練習1
public class StringExer1 {
public static void main(String[] args) {
//String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
//在上一行代碼執行完以後,字元串常量池中并沒有"ab"
String s2 = s.intern();//jdk6中:在串池中建立一個字元串"ab"
//jdk8中:串池中沒有建立字元串"ab",而是建立一個引用,指向new String("ab"),将此引用傳回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
jdk6
jdk7/8
5.6 練習2
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("ab");//執行完以後,會在字元串常量池中會生成"ab"
// String s1 = new String("a") + new String("b");執行完以後,不會在字元串常量池中會生成"ab"
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2); //false
}
}
5.7 intern()效率測試
大的網站平台,需要記憶體中存儲大量的字元串。比如社交網站,很多人都存儲:北京市、海澱區等資訊。這時候如果字元串都調用 intern()方法,就會明顯降低記憶體的大小。
/**
* 使用intern()測試執行效率:空間使用上
*
* 結論:對于程式中大量存在存在的字元串,尤其其中存在很多重複字元串時,使用intern()可以節省記憶體空間。
*
*/
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();
}
System.gc();
}
}
arr[i] = new String(String.valueOf(data[i % data.length]));
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
6.StrtingTable的垃圾回收
/**
* String的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
*/
public class StringGCTest {
public static void main(String[] args) {
// for (int j = 0; j < 100; j++) {
// String.valueOf(j).intern();
// }
//發生垃圾回收行為
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
7.G1中的String去重操作
背景:對許多Java應用(有大的也有小的)做的測試得出以下結果:
➢堆存活資料集合裡面String對象占了25%
➢堆存活資料集合裡面重複的String對象有13.5%
➢String對象的平均長度是45
許多大規模的Java應用的瓶頸在于記憶體,測試表明,在這些類型的應用裡面,Java堆中存活的資料集合差不多258是String對象。更進一一步,這裡面差不多一半String對象是重複的,重複的意思是說:string1. equals (string2)=true。堆上存在重複的string對象必然是一種記憶體的浪費。這個項目将在G1垃圾收集器中實作自動持續對重複的String對象進行去重,這樣就能避免浪費記憶體。
實作
➢當垃圾收集器工作的時候,會通路堆上存活的對象。對每一個通路的對象都會檢查是否是候選的要去重的String對象。
➢如果是,把這個對象的一個引用插入到隊列中等待後續的處理。一個去重的線程在背景運作,處理這個隊列。處理隊列的一個元素意味着從隊列删除這個元素,然後嘗試去重它引用的String對象。
➢使用一個hashtable來記錄所有的被String對象使用的不重複的char數組。
當去重的時候,會查這個hashtable,來看堆上是否已經存在一個一模一樣的char數組。
➢如果存在,String對象會被調整引用那個數組,釋放對原來的數組的引用,最終會被垃圾收集器回收掉。
➢如果查找失敗,char數組會被插入到hashtable,這樣以後的時候就可以共享這個數組了。
指令行選項
➢UseStringDeduplication (bool) :開啟String去重,預設是不開啟的,需要手動開啟。
➢PrintStringDedupl icationStatistics (bool) :列印詳細的去重統計資訊,
➢StringDedupl icationAgeThreshold (uintx) :達到這個年齡的string對象被認.為是去重的候選對象