1.字符串常量池
1.1. 创建对象的思考
下面两种创建String对象的方式相同吗?
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s3 == s4); // false
}
上述程序创建方式类似,为什么s1和s2引用的是同一个对象,而s3和s4不是呢?
在Java中,字符串常量池是一种特殊的运行时常量池,用于存储String类的字符串字面常量。字符串常量池的主要目的是避免重复创建相同的字符串对象,从而节省内存和提高运行效率。
当在Java程序中使用字符串字面常量(如"hello")时,如果字符串常量池中已经存在相同内容的字符串对象,则不会创建新的对象,而是直接引用已存在的对象。这样做可以减少内存消耗,特别是在字符串频繁创建和使用的情况下。
在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,Java为8种基本数据类型和String类都提供了常量池。
“池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 “内存池”, “线程池”, “数据库连接池” …
比如:家里给大家打生活费的方式
- 家里经济拮据,每月定时打生活费,有时可能会晚,最差情况下可能需要向家里张口要,速度慢
- 家里有矿,一次性打一年的生活费放到银行卡中,自己随用随取,速度非常快
方式2,就是池化技术的一种示例,钱放在卡上,随用随取,效率非常高。常见的池化技术比如:数据库连接池、线程池等。
为了节省存储空间以及程序的运行效率,Java中引入了:
- Class文件常量池:每个.Java源文件编译后生成.Class文件中会保存当前类中的字面常量以及符号信息
- 运行时常量池:在.Class文件被加载时,.Class文件中的常量池被加载到内存中称为运行时常量池,运行时常量池每个类都有一份
- 字符串常量池:字符串常量池是运行时常量池中的一部分。在Java中,字符串是不可变的对象,为了节省内存并提高性能,字符串常量池被引入。当你创建一个字符串常量时(例如使用双引号括起来的字符串字面值),Java会首先检查字符串常量池中是否已经有相同值的字符串对象。如果有,则返回常量池中的对象引用,而不是创建一个新的字符串对象。这样可以避免创建重复的字符串对象,从而节省内存空间。
使用字符串常量池的优势在于:
-
节省内存:避免了创建大量相同内容的字符串对象,降低了内存占用。
-
提高效率:由于字符串常量池中的对象是可复用的,当需要创建相同内容的字符串时,可以直接引用常量池中的对象,避免了对象的重复创建和销毁过程,从而提高了程序的执行效率。
注意:虽然字符串常量池在一定程度上提高了程序的性能,但过度的使用字符串常量可能会导致一些问题。例如,如果在程序中大量使用字符串常量拼接,每次拼接都会生成一个新的String对象,这种情况下使用StringBuilder或StringBuffer等可变字符串类更合适,因为它们可以避免创建过多临时的String对象。
2.2. 字符串常量池(StringTable)
字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构),不同JDK版本下字符串常量池的位置以及默认大小是不同的:
JDK版本 | 字符串常量池位置 | 大小设置 |
---|---|---|
Java6 | (方法区)永久代 | 固定大小:1009 |
Java7 | 堆中 | 可设置,没有大小限制,默认大小:60013 |
Java8 | 堆中 | 可设置,有范围限制,最小是1009 |
1.3. 再谈String对象创建
由于不同JDK版本对字符串常量池的处理方式不同,此处在Java8 HotSpot上分析
- 直接使用字符串常量进行赋值
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true
}
在上述代码中有两个字符串字面常量 “hello”,那么它们都会被放入字符串常量池,并且只有一个实际的String对象用于表示这个内容。这样,当程序中其他部分使用了相同的字符串字面常量 “hello”,都会共享这个已存在的String对象。
存储字符串常量的时候,会先检查当前常量池是否存在你所要存储的常量。
- 通过new创建String类对象
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s3 == s4); // false
}
常量池存在“hello”,此时不在常量中存储hello,此时只需要重新创建一个String对象那个,将它的value域存放hello数组的地址,指向那个hello数组
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("world");
String s4 = new String("world");
}
常量池不存在“world”,此时需要在常量中存储world。
结论:只要是new的对象,都是唯一的。
通过上面例子可以看出,使用常量串创建String类型对象的效率更高,而且更节省空间,用户也可以将创建的字符串对象通过intern方式添加进字符串常量池中。
1.4. intern方法
intern 是一个native方法(Native方法指:底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中。
public static void main(String[] args) {
char[] ch = new char[]{'a', 'b', 'c'};
String s1 = new String(ch); // s1对象并不在常量池中
//s1.intern(); // s1.intern();调用之后,会将s1对象的引用放入到常量池中
String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
System.out.println(s1 == s2);// 输出false
}
// 将上述方法打开之后,就会输出true
当代码运行到String s1 = new String(ch);时,我们发现在常量池中没有任何的数据,继续运行
public static void main(String[] args) {
char[] ch = new char[]{'a', 'b', 'c'};
String s1 = new String(ch); // s1对象并不在常量池中
s1.intern(); // s1.intern();调用之后,会将s1对象的引用放入到常量池中
String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
System.out.println(s1 == s2);// 将方法打开之后,就会输出true
}
需要注意的是,虽然 intern() 方法可以减少重复字符串对象的内存占用,但滥用它也可能导致常量池过度膨胀,增加内存开销。在某些情况下,过度使用 intern() 方法可能会影响性能,因此在使用时需要谨慎考