对问题的解释
1. “字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table” 的含义
这句话可以从以下几个方面理解:
(1) 字符串常量池的存储内容
- 直接引用:字符串常量池中存储的是指向实际
String
对象的引用(即内存地址),而不是直接存储String
对象本身。 - 不是直接存放对象:实际的
String
对象仍然存在于堆内存中,字符串常量池仅保存这些对象的引用(类似“索引表”)。
(2) “string table” 的作用
- 结构:字符串常量池内部通常以哈希表(Hash Table) 的形式实现,称为
string table
。 - 功能:通过哈希表快速查找是否存在与某个字符串内容相同的对象:
- 当程序中出现字符串字面量(如
"hello"
)时,JVM 会检查string table
中是否存在该字符串的引用。 - 如果存在,则直接返回该引用;如果不存在,则创建新对象并将其引用存入
string table
。
- 当程序中出现字符串字面量(如
(3) 示例说明
String s1 = "hello"; // 1. 检查常量池是否有"hello",没有则创建并存入
String s2 = "hello"; // 2. 直接返回常量池中的引用,s1和s2指向同一个对象
- 内存分布:
- 常量池中存储的是
"hello"
的引用(如地址0x1000
)。 - 实际的
"hello"
对象在堆内存中,内容包含字符数组{'h', 'e', 'l', 'l', 'o'}
。
- 常量池中存储的是
2. 与缓存的关系
字符串常量池本质上是一种缓存机制,其核心目的是复用已有对象,从而节省内存和提升性能:
(1) 缓存的实现原理
- 避免重复创建对象:当多个相同字符串被使用时,通过常量池共享同一个对象,减少内存占用。
String s3 = new String("world"); // 1. 在堆中创建新对象,不放入常量池 String s4 = new String("world"); // 2. 再次创建新对象,导致内存浪费 System.out.println(s3 == s4); // false(两个不同对象)
- 通过
intern()
手动缓存:若希望new String()
创建的对象也进入常量池,需调用intern()
方法:String s5 = new String("test").intern(); // 将对象加入常量池 String s6 = "test"; // 直接指向常量池中的对象 System.out.println(s5 == s6); // true(同一个对象)
(2) 缓存的优势
- 节省内存:相同内容的字符串只占用一份内存空间。
- 提升性能:
- 快速查找:通过哈希表(
string table
)快速判断字符串是否存在。 - 避免垃圾回收压力:减少短命对象的频繁创建和销毁。
- 快速查找:通过哈希表(
(3) 与 JVM 内存区域的关系
- JDK 7 之前:字符串常量池位于方法区(Method Area)。
- JDK 7 及以后:字符串常量池移至堆内存(Heap),与普通对象存储在同一区域,但通过
string table
管理引用。
3. 总结
- 字符串常量池的本质:通过存储对象的引用(而非对象本身)实现一种缓存机制,利用哈希表(
string table
)快速复用相同内容的字符串对象。 - 与缓存的关系:它是一种内存优化策略,通过减少对象重复创建来节省内存和提升性能,类似于其他缓存机制(如
Integer
的缓存区间)。
常见疑问解答
-
为什么
new String("abc")
不进入常量池?- 因为
new
总是创建新对象,且不自动调用intern()
。若需缓存,需显式调用intern()
。
- 因为
-
intern()
的作用是什么?- 将字符串对象加入常量池,确保相同内容的字符串共享同一引用。
-
字符串拼接是否会影响常量池?
- 字符串拼接(如
"a" + "b"
)会生成新对象,但若结果是编译期可知的字面量,则会被自动加入常量池:String s = "a" + "b"; // 编译时合并为"ab",直接使用常量池中的对象
- 字符串拼接(如
通过理解字符串常量池的设计,可以更好地优化代码中字符串的使用,避免内存浪费并提升性能。
例子解释
1. 缓存范围内的值(-128 到 127)
Integer a = 100; // 自动装箱,调用 Integer.valueOf(100)
Integer b = 100; // 再次调用 Integer.valueOf(100),返回缓存中的同一个对象
System.out.println(a == b); // 输出 true(指向同一对象)
- 原因:
a
和b
的值100
在缓存范围内(-128 到 127)。
Integer.valueOf(100)
会直接从缓存数组中获取已存在的Integer
对象,因此a
和b
指向同一个对象,==
比较返回true
。
2. 超出缓存范围的值(如 128 或 -129)
Integer c = 128; // 自动装箱,调用 Integer.valueOf(128)
Integer d = 128; // 调用 Integer.valueOf(128),超出默认缓存范围
System.out.println(c == d); // 输出 false(指向不同对象)
- 原因:
128
超出默认缓存范围(-128 到 127)。
Integer.valueOf(128)
会新建一个Integer
对象,因此c
和d
是两个不同的对象,==
比较返回false
。
3. 使用 new
创建对象(绕过缓存)
Integer e = new Integer(100); // 显式 new 对象,绕过缓存
Integer f = new Integer(100); // 再次 new,创建新对象
System.out.println(e == f); // 输出 false(即使值相同,也是不同对象)
- 原因:
使用new
关键字会直接创建新对象,完全绕过缓存机制。因此e
和f
是不同的对象,==
返回false
。
关键点总结
场景 | 行为 | == 结果 | 原因 |
---|---|---|---|
值在缓存范围内(如 100) | 自动装箱调用 Integer.valueOf() ,返回缓存中的同一个对象。 | true | a 和 b 指向同一对象。 |
值超出范围(如 128) | 自动装箱调用 Integer.valueOf() ,但超出缓存范围,因此每次新建对象。 | false | c 和 d 是不同的对象。 |
使用 new 创建对象 | 显式调用构造函数,绕过缓存,每次创建新对象。 | false | e 和 f 是不同的对象。 |
补充说明
1. 如何比较值的大小?
- 使用
equals()
方法:System.out.println(c.equals(d)); // 输出 true(比较值,而非引用) System.out.println(e.equals(f)); // 输出 true(值相同)
- 避免
==
比较:
==
比较的是对象的引用(内存地址),而equals()
比较的是对象的值。
2. 缓存范围的调整
- 通过 JVM 参数调整:
可以通过-XX:AutoBoxCacheMax=200
或设置系统属性java.lang.Integer.IntegerCache.high=200
来扩展缓存范围。
示例:// 假设将缓存范围调整为 -128 到 200 Integer g = 150; // 自动装箱,命中缓存 Integer h = 150; // 返回缓存中的对象 System.out.println(g == h); // 输出 true
完整代码示例
public class IntegerCacheExample {
public static void main(String[] args) {
// 场景1:值在缓存范围内
Integer a = 100;
Integer b = 100;
System.out.println("a == b: " + (a == b)); // true
// 场景2:值超出缓存范围
Integer c = 128;
Integer d = 128;
System.out.println("c == d: " + (c == d)); // false
// 场景3:使用 new 创建对象
Integer e = new Integer(100);
Integer f = new Integer(100);
System.out.println("e == f: " + (e == f)); // false
// 使用 equals 比较值
System.out.println("c.equals(d): " + c.equals(d)); // true
System.out.println("e.equals(f): " + e.equals(f)); // true
}
}
输出结果
a == b: true
c == d: false
e == f: false
c.equals(d): true
e.equals(f): true
通过这个例子,可以清晰地看到 Integer 缓存机制的作用和不同场景下的行为差异。