Java字符串常量池(String Constant Pool)原理、實(shí)現(xiàn)全解析
字符串常量池(String Constant Pool)
字符串常量池是 Java 中用于優(yōu)化字符串內(nèi)存使用的核心機(jī)制,屬于 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)的重要組成部分。其本質(zhì)是一個(gè)字符串駐留池(String Intern Pool),核心目標(biāo)是復(fù)用相同內(nèi)容的字符串對(duì)象,減少內(nèi)存開銷,提升性能。下面從底層原理、JVM 存儲(chǔ)位置、創(chuàng)建機(jī)制、地址比較、intern 方法、常見問題等維度,全面解析字符串常量池的所有內(nèi)容。
一、字符串常量池的底層本質(zhì)與設(shè)計(jì)初衷
1. 設(shè)計(jì)初衷:解決字符串的內(nèi)存浪費(fèi)問題
字符串是 Java 中最常用的對(duì)象之一,若每次創(chuàng)建相同內(nèi)容的字符串都新建對(duì)象,會(huì)導(dǎo)致大量重復(fù)對(duì)象占用內(nèi)存。例如:
// 若沒有常量池,會(huì)創(chuàng)建1000個(gè)內(nèi)容為"hello"的字符串對(duì)象
for (int i = 0; i < 1000; i++) {
String s = "hello";
}
字符串常量池通過 **“駐留(Intern)” 機(jī)制 **,讓相同內(nèi)容的字符串僅保留一份副本,所有指向該內(nèi)容的字符串引用都指向這個(gè)副本,從而節(jié)省內(nèi)存。
2. 本質(zhì):一個(gè)哈希表(HashTable)
字符串常量池的底層實(shí)現(xiàn)是一個(gè)固定大小的哈希表(JDK1.7+),默認(rèn)容量為 1009(可通過 JVM 參數(shù)-XX:StringTableSize調(diào)整,JDK1.8 后默認(rèn)是 60013)。
- 哈希表的key:字符串的內(nèi)容(字符序列)。
- 哈希表的value:字符串對(duì)象的引用(JDK1.7+)或字符串對(duì)象本身(JDK1.6 及以前)。
- 哈希表的作用:快速查找是否存在相同內(nèi)容的字符串,時(shí)間復(fù)雜度接近 O (1)。
二、字符串常量池的存儲(chǔ)位置(JDK 版本差異)
字符串常量池的存儲(chǔ)位置在不同 JDK 版本中存在重大變化,這是理解其機(jī)制的關(guān)鍵:
| JDK 版本 | 存儲(chǔ)位置 | 存儲(chǔ)內(nèi)容 | 關(guān)鍵說明 |
|---|---|---|---|
| JDK1.6 及以前 | 方法區(qū)(永久代,PermGen) | 字符串對(duì)象本身(字符數(shù)組 + 屬性) | 永久代內(nèi)存空間有限,容易出現(xiàn)OutOfMemoryError: PermGen space。 |
| JDK1.7+ | 堆內(nèi)存(Heap) | 字符串對(duì)象的引用(指向堆中的字符串對(duì)象) | ① 常量池從永久代移到堆,解決永久代內(nèi)存限制;② 存儲(chǔ)的是引用,而非對(duì)象本身,更節(jié)省內(nèi)存。 |
| JDK1.8+ | 堆內(nèi)存(Heap) | 字符串對(duì)象的引用 | JDK1.8 移除永久代,方法區(qū)改為元空間(Metaspace),常量池仍在堆中。 |
關(guān)鍵差異示例(JDK1.6 vs JDK1.7+)
String s = new String("abc");
// JDK1.6:常量池在永久代,存儲(chǔ)"abc"對(duì)象;堆中存儲(chǔ)另一個(gè)"abc"對(duì)象,s指向堆對(duì)象。
// JDK1.7+:常量池在堆中,存儲(chǔ)堆中"abc"對(duì)象的引用;堆中有一個(gè)"abc"對(duì)象(字面量),s指向堆中新建的另一個(gè)"abc"對(duì)象。
三、字符串的創(chuàng)建方式與常量池的交互
字符串的創(chuàng)建方式直接決定了其與常量池的交互邏輯,也是判斷 “地址是否相同” 的核心依據(jù)。
場(chǎng)景 1:直接使用字符串字面量創(chuàng)建(String s = "abc")
這是最常見的方式,完全依賴常量池的駐留機(jī)制。
執(zhí)行流程(JDK1.7+):
- JVM 首先檢查字符串常量池的哈希表,是否存在鍵為 “abc” 的條目。
- 若不存在:
- 在堆內(nèi)存中創(chuàng)建一個(gè)字符串對(duì)象(內(nèi)容為 “abc”)。
- 將該對(duì)象的引用存入字符串常量池的哈希表中(key=“abc”,value = 對(duì)象引用)。
- 將引用變量
s指向堆中的這個(gè)字符串對(duì)象。
- 若存在:
- 直接從常量池的哈希表中取出 “abc” 對(duì)應(yīng)的對(duì)象引用。
- 將引用變量
s指向該引用對(duì)應(yīng)的堆對(duì)象。
代碼示例:
String s1 = "abc"; String s2 = "abc"; System.out.println(s1 == s2); // true(指向同一個(gè)堆對(duì)象) System.out.println(s1.equals(s2)); // true(內(nèi)容相同)
場(chǎng)景 2:使用new String("內(nèi)容")創(chuàng)建
這種方式會(huì)創(chuàng)建至少一個(gè)、最多兩個(gè)字符串對(duì)象,是最容易產(chǎn)生內(nèi)存浪費(fèi)的方式。
執(zhí)行流程(JDK1.7+):
- 處理字符串字面量
"abc":JVM 檢查常量池,若沒有則在堆中創(chuàng)建 “abc” 對(duì)象,并將引用存入常量池(這是第一個(gè)對(duì)象)。 - 執(zhí)行
new String("abc"):在堆內(nèi)存中新建一個(gè)獨(dú)立的字符串對(duì)象(這是第二個(gè)對(duì)象),該對(duì)象的字符數(shù)組與常量池中的 “abc” 對(duì)象共享(JDK1.7 + 優(yōu)化,字符數(shù)組存儲(chǔ)在堆中,復(fù)用字符數(shù)組以節(jié)省內(nèi)存)。 - 引用變量
s指向第二步新建的堆對(duì)象。
代碼示例:
String s1 = "abc"; // 常量池存引用,s1指向堆中的"abc"對(duì)象
String s2 = new String("abc"); // 堆中新建對(duì)象,s2指向該對(duì)象
String s3 = new String("abc"); // 堆中再新建對(duì)象,s3指向該對(duì)象
System.out.println(s1 == s2); // false(指向不同堆對(duì)象)
System.out.println(s2 == s3); // false(指向不同堆對(duì)象)
System.out.println(s1.equals(s2)); // true(內(nèi)容相同)
System.out.println(s2.intern() == s1); // true(intern()返回常量池引用)關(guān)鍵:new String("abc")創(chuàng)建的對(duì)象數(shù)量
- 最少 1 個(gè):若常量池中已存在 “abc” 的引用,則僅在堆中創(chuàng)建一個(gè)新對(duì)象。
- 最多 2 個(gè):若常量池中不存在 “abc” 的引用,則先在堆中創(chuàng)建 “abc” 對(duì)象(供常量池引用),再在堆中創(chuàng)建一個(gè)新對(duì)象(供
s引用)。
場(chǎng)景 3:字符串拼接(編譯期常量拼接 vs 運(yùn)行期拼接)
字符串拼接的結(jié)果是否指向常量池對(duì)象,取決于拼接的是常量還是變量。
子場(chǎng)景 3.1:編譯期常量拼接(字面量 /final常量拼接)
特征:拼接的所有部分都是編譯期就能確定值的常量(字符串字面量、final字符串常量、基本類型常量)。原理:Java 編譯器會(huì)對(duì)這種拼接進(jìn)行編譯期優(yōu)化,直接將拼接結(jié)果作為一個(gè)字符串字面量存入常量池,運(yùn)行時(shí)不再執(zhí)行拼接操作。
代碼示例:
// 示例1:字面量拼接
String s1 = "abc";
String s2 = "ab" + "c"; // 編譯期優(yōu)化為"abc"
String s3 = "a" + "b" + "c"; // 編譯期優(yōu)化為"abc"
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
// 示例2:final常量拼接
final String str1 = "ab"; // final常量,編譯期確定值
final int num = 123; // 基本類型常量
String s4 = str1 + "c"; // 編譯期優(yōu)化為"abc"
String s5 = "num" + num; // 編譯期優(yōu)化為"num123"
System.out.println(s1 == s4); // true
System.out.println("num123" == s5); // true子場(chǎng)景 3.2:運(yùn)行期拼接(變量拼接)
特征:拼接的部分包含運(yùn)行期才能確定值的變量(非final字符串變量、對(duì)象引用等)。原理:這種拼接會(huì)在運(yùn)行期通過StringBuilder的append()方法拼接,再調(diào)用toString()方法生成新的字符串對(duì)象,該對(duì)象是堆中的新對(duì)象,與常量池?zé)o關(guān)。
代碼示例:
String s1 = "abc";
String s2 = "ab";
String s3 = s2 + "c"; // 運(yùn)行期拼接,底層用StringBuilder實(shí)現(xiàn)
String s4 = new String("ab") + new String("c"); // 運(yùn)行期拼接,堆新對(duì)象
System.out.println(s1 == s3); // false(s3指向堆新對(duì)象)
System.out.println(s1 == s4); // false(s4指向堆新對(duì)象)
System.out.println(s3 == s4); // false(兩個(gè)不同的堆對(duì)象)場(chǎng)景 4:使用String.intern()方法(手動(dòng)駐留字符串)
intern()是 String 類的 native 方法(底層由 C/C++ 實(shí)現(xiàn)),作用是將字符串對(duì)象的內(nèi)容駐留到常量池,并返回常量池中的引用。這是手動(dòng)控制字符串常量池的核心方法。
// 案例1:new String() + intern()
String s1 = "abc";
String s2 = new String("abc").intern();
System.out.println(s1 == s2); // true(s2指向常量池中的對(duì)象)
// 案例2:運(yùn)行期拼接 + intern()
String s3 = "ab";
String s4 = s3 + "c"; // 堆中新對(duì)象
String s5 = s4.intern(); // 將s4的內(nèi)容"abc"加入常量池(已存在,返回引用)
System.out.println(s1 == s5); // true
System.out.println(s1 == s4); // false(s4還是堆中的對(duì)象)
// 案例3:new String() + intern()
String s1 = "abc";
String s2 = new String("abc").intern();
System.out.println(s1 == s2); // true(s2指向常量池中的對(duì)象)
// 案例4:運(yùn)行期拼接 + intern()
String s3 = "ab";
String s4 = (s3 + "c").intern(); // 修正:補(bǔ)充intern()的括號(hào)
System.out.println(s4 == s1); // 結(jié)果:true
}
}原理:
- 調(diào)用
intern()時(shí),JVM 檢查常量池:- 若常量池中已有該內(nèi)容的字符串,直接返回常量池中的引用。
- 若沒有,將當(dāng)前字符串的內(nèi)容加入常量池(JDK1.7 + 是將堆中對(duì)象的引用存入常量池,JDK1.6 是復(fù)制對(duì)象到常量池),并返回常量池中的引用。結(jié)論:
intern()可以讓運(yùn)行期創(chuàng)建的字符串指向常量池的地址,實(shí)現(xiàn) “內(nèi)容相同則地址相同”。
執(zhí)行流程(JDK1.7+):
- 調(diào)用
s.intern()時(shí),JVM 檢查字符串常量池的哈希表:- 若存在鍵為
s內(nèi)容的條目:直接返回該條目對(duì)應(yīng)的引用。 - 若不存在:將
s的引用存入常量池的哈希表(key=s 的內(nèi)容,value=s 的引用),并返回s的引用。
- 若存在鍵為
- JDK1.6 與 JDK1.7 + 的關(guān)鍵差異:
- JDK1.6:若常量池中不存在該字符串,會(huì)復(fù)制一份字符串對(duì)象到永久代的常量池,并返回永久代中對(duì)象的引用。
- JDK1.7+:若常量池中不存在該字符串,會(huì)將堆中對(duì)象的引用存入常量池,而非復(fù)制對(duì)象,更節(jié)省內(nèi)存。
代碼示例(JDK1.7+):
// 示例1:new String() + intern()
String s1 = new String("abc"); // 堆對(duì)象1,常量池存"abc"的引用(指向堆中字面量對(duì)象)
String s2 = s1.intern(); // 常量池已有"abc"引用,返回該引用
System.out.println(s1 == s2); // false(s1指向堆對(duì)象1,s2指向常量池引用的對(duì)象)
// 示例2:運(yùn)行期拼接 + intern()
String s3 = new String("ab") + new String("c"); // 堆對(duì)象2,內(nèi)容為"abc",常量池?zé)o該引用
String s4 = s3.intern(); // 將s3的引用存入常量池,返回s3的引用
System.out.println(s3 == s4); // true(s4指向s3的堆對(duì)象2)
System.out.println(s4 == "abc"); // true("abc"指向常量池中的s3引用)
// 示例3:字面量 + intern()
String s5 = "hello";
String s6 = s5.intern();
System.out.println(s5 == s6); // true(常量池已有引用,直接返回)應(yīng)用場(chǎng)景:海量字符串去重
當(dāng)處理大量重復(fù)字符串(如日志、數(shù)據(jù)庫(kù)數(shù)據(jù))時(shí),調(diào)用intern()方法可將重復(fù)字符串的引用指向常量池,大幅減少內(nèi)存占用。例如:
// 假設(shè)有100萬個(gè)內(nèi)容為"user_123"的字符串
String s = new String("user_123").intern();
// 后續(xù)所有"user_123"的字符串都調(diào)用intern(),則僅占用一份內(nèi)存
四、字符串常量池的地址比較:核心規(guī)則
Java 中判斷地址是否相同用==(比較引用),判斷內(nèi)容是否相同用equals()(比較字符序列)。結(jié)合常量池的機(jī)制,地址比較的核心規(guī)則如下:
| 字符串創(chuàng)建方式 | 內(nèi)容相同時(shí),地址是否相同(==) | 底層原因 |
|---|---|---|
字符串字面量("abc") | 是 | 常量池自動(dòng)復(fù)用相同內(nèi)容的引用。 |
new String("abc") | 否 | 每次new都創(chuàng)建堆新對(duì)象,與常量池引用無關(guān)。 |
編譯期常量拼接("ab"+"c") | 是 | 編譯器優(yōu)化為字面量,常量池復(fù)用引用。 |
運(yùn)行期變量拼接(s2+"c") | 否 | 底層StringBuilder生成堆新對(duì)象。 |
new String("abc").intern() | 是 | intern()返回常量池中的引用,與字面量引用相同。 |
運(yùn)行期拼接 + intern() | 是(JDK1.7+) | intern()將堆對(duì)象引用存入常量池,后續(xù)字面量指向該引用。 |
五、字符串常量池的常見問題與誤區(qū)
誤區(qū) 1:“字符串常量池存儲(chǔ)的是字符串對(duì)象”
糾正:JDK1.7 + 常量池存儲(chǔ)的是字符串對(duì)象的引用,而非對(duì)象本身;JDK1.6 及以前存儲(chǔ)的是對(duì)象本身。
- 原因:JDK1.7 + 將常量池移到堆中,存儲(chǔ)引用可避免復(fù)制對(duì)象,節(jié)省內(nèi)存。
誤區(qū) 2:“String s = new String("abc")創(chuàng)建了一個(gè)對(duì)象”
糾正:最多創(chuàng)建兩個(gè)對(duì)象(常量池的字面量對(duì)象 + 堆的新對(duì)象),最少創(chuàng)建一個(gè)對(duì)象(常量池已有字面量,僅堆新對(duì)象)。
誤區(qū) 3:“intern()方法一定能節(jié)省內(nèi)存”
糾正:intern()的哈希表是固定大小的,若大量調(diào)用intern()存儲(chǔ)不同內(nèi)容的字符串,會(huì)導(dǎo)致哈希表沖突,性能下降(從 O (1) 變?yōu)?O (n))。因此:
- 適合大量重復(fù)字符串的場(chǎng)景(如去重)。
- 不適合大量唯一字符串的場(chǎng)景(如 UUID),會(huì)浪費(fèi)常量池空間,降低性能。
誤區(qū) 4:“字符串是不可變的,所以常量池中的字符串也不能變”
糾正:字符串的不可變是指字符數(shù)組被final修飾,且沒有提供修改字符的方法(如setCharAt()),但通過反射可以修改字符數(shù)組的內(nèi)容(不推薦)。例如:
String s = "abc";
// 通過反射修改字符數(shù)組
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] value = (char[]) valueField.get(s);
value[0] = 'x';
System.out.println(s); // 輸出:xbc
System.out.println("abc"); // 輸出:xbc(常量池中的引用指向同一個(gè)對(duì)象,因此也被修改)
注意:這種操作會(huì)破壞常量池的駐留機(jī)制,導(dǎo)致所有指向該字符串的引用都被修改,屬于高危操作,禁止在生產(chǎn)環(huán)境使用。
誤區(qū) 5:“空字符串""也在常量池中”
糾正:空字符串""是一個(gè)合法的字符串字面量,會(huì)被駐留到常量池中。例如:
String s1 = ""; String s2 = ""; System.out.println(s1 == s2); // true(常量池復(fù)用引用)
六、字符串常量池的 JVM 參數(shù)調(diào)優(yōu)
1. 調(diào)整字符串常量池的哈希表大小
JVM 參數(shù):-XX:StringTableSize=<size>
- JDK1.6:默認(rèn)大小 1009,最大可設(shè)為 65535。
- JDK1.7+:默認(rèn)大小 60013(JDK1.8),可設(shè)為更大的質(zhì)數(shù)(如 1000003),減少哈希沖突。
- 適用場(chǎng)景:當(dāng)系統(tǒng)中存在大量
intern()調(diào)用時(shí),增大哈希表大小可提升查找性能。
2. 開啟字符串去重(JDK1.8u20+)
JVM 參數(shù):-XX:+UseStringDeduplication
- 作用:在 G1 垃圾收集器中,自動(dòng)檢測(cè)堆中重復(fù)的字符串對(duì)象,讓它們共享同一個(gè)字符數(shù)組,進(jìn)一步節(jié)省內(nèi)存。
- 區(qū)別:與常量池的駐留機(jī)制不同,該參數(shù)是在堆中對(duì)字符串對(duì)象的字符數(shù)組去重,而常量池是對(duì)引用的駐留。
七、總結(jié)
字符串常量池是 Java 優(yōu)化字符串內(nèi)存的核心機(jī)制,其關(guān)鍵要點(diǎn)可歸納為:
- 存儲(chǔ)位置:JDK1.6 在永久代,JDK1.7 + 在堆中。
- 底層實(shí)現(xiàn):哈希表,存儲(chǔ)字符串內(nèi)容與引用的映射(JDK1.7+)。
- 核心機(jī)制:駐留(Intern),相同內(nèi)容的字符串僅保留一份引用。
- 創(chuàng)建方式:字面量創(chuàng)建復(fù)用常量池,
new String()創(chuàng)建新對(duì)象,拼接分為編譯期和運(yùn)行期。 - intern 方法:手動(dòng)將字符串駐留到常量池,JDK1.7 + 存儲(chǔ)引用,JDK1.6 存儲(chǔ)對(duì)象。
- 地址比較:
==比較引用,equals()比較內(nèi)容,常量池中的字符串引用相同。
理解字符串常量池的機(jī)制,不僅能避免內(nèi)存浪費(fèi),還能在面試和生產(chǎn)環(huán)境中解決字符串相關(guān)的性能問題。
到此這篇關(guān)于Java字符串常量池(String Constant Pool)原理、實(shí)現(xiàn)全解析的文章就介紹到這了,更多相關(guān)java字符串常量池內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談SpringMVC中的session用法及細(xì)節(jié)記錄
下面小編就為大家?guī)硪黄獪\談SpringMVC中的session用法及細(xì)節(jié)記錄。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
SpringBoot使用@Async注解實(shí)現(xiàn)異步調(diào)用
這篇文章主要介紹了SpringBoot使用@Async注解實(shí)現(xiàn)異步調(diào)用,異步調(diào)用是相對(duì)于同步調(diào)用而言的,同步調(diào)用是指程序按預(yù)定順序一步步執(zhí)行,每一步必須等到上一步執(zhí)行完后才能執(zhí)行,異步調(diào)用則無需等待,程序執(zhí)行完即可執(zhí)行,可以減少程序執(zhí)行時(shí)間,需要的朋友可以參考下2023-10-10
java編程中自動(dòng)拆箱與自動(dòng)裝箱詳解
這篇文章主要介紹了java編程中自動(dòng)拆箱與自動(dòng)裝箱詳解,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
Java替換字符串replace和replaceAll方法舉例詳解
這篇文章主要介紹了Java中替換字符串的幾種方法,包括String類的replace()、replaceAll()、replaceFirst()方法,以及StringBuilder和StringBuffer類的replace()方法,還提到了一些第三方庫(kù),如Hutool,它們提供了更豐富的字符串處理功能,需要的朋友可以參考下2025-02-02
String實(shí)例化及static final修飾符實(shí)現(xiàn)方法解析
這篇文章主要介紹了String實(shí)例化及static final修飾符實(shí)現(xiàn)方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09

