前言
-
String s1 = new String("zero");
内存分配在哪里? -
String s2 = "zero";
内存又分配在哪里? -
s1.intern();
干了啥? -
String 类型的"+"到底发生了什么?
-
常量折叠是什么?
-
不知道没事,这不是可以学嘛~
String 常量池是啥?
- 比较官方的解释是:字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,
主要目的是为了避免字符串的重复创建
。 - 因为 String 类型在设计的时候使用了final修饰的字符数组来保存字符串的,并且没有提供提供修改数组内容的方法,
所以 String 类型的对象被创建的话,是不允许修改的
。 - 如果你频繁的对 String 类型进行“+”运算,其实并不会直接在原对象上进行修改,而是会创建一个新的 String 对象,具体原理会在后面详细说明,这里大家就简单了解一下先。
- 这个时候出现了一种优化的方式,那就是
字符串常量池StringBuilder 和 StringBuffer 这些类提供了对字符串内容频繁修改的方法,可以实现因频繁修改造成的内存浪费。
- 那字符串常量池有啥用啊?
- 咳咳,其实吧,当你创建字符串常量的时候(
用双引号创建字符串
),会先去访问到字符串常量池,看看里面有没有相同值的字符串,有的话就会直接返回,没有的话就在常量池里面整一份,然后给你。 注意这里的说法
,是创建字符串常量,也就是使用双引号创建的字符串,不是使用 new 关键字创建的字符串对象,因为如果使用 new 关键字,通过 String 类型提供的构造器创建的字符串对象,会存放在堆内存中,并不会在常量池里面缓存。- 这也就回答了在前言中的第一个和第二个疑问。
String s1 = new String("zero");
在堆内存创建了对象String s2 = "zero";
内存又分配在字符串常量池
关于intern()
- 相信很多小萌新对这个方法还是很陌生的,这方法有啥用,甚至连这个方法是 String 类型的都不知道。
- 说人话版本就是分两种情况嘛:
a. 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
b. 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。 - 所以我这
建议好好说话觉得大佬的阐述非常严谨。
String 类型的"+"到底发生了什么?
- 上面也说了,String 类型使用+运算符的话会产生行的字符串对象。
- 更清楚的版本的话,其实就和 StringBuilder 有关系了,不清楚 StringBuilder 的小伙伴们可以去了解一下,比较简单,学起来也比较快。
实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后,调用 toString() 得到一个 String 对象 。
- 所以,这里的"+"产生的对象,可不止有新的 String 对象哦,还有工具人 StringBuilder 。
那如果是通过“+”运算符得到的字符串对象到底存放在哪里呢?
- 咱们先来看一段代码
String s1 = "ze";
String s2 = "ro";
String s3 = "ze" + "ro";
String s4 = s1 + s2;
String s5 = "zero";
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s4 == s5);//false
- 在解释这些之前我们需要了解一个知识点,
常量折叠
。
常量折叠
- 常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一。
但是
,并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以
。- 比如,八大基本数据类型、字符串常量、被 final 修饰的基本数据类型变量和字符串变量,以及前面四种的运算(字符串的运算理解成“+”就好了)的表达式,就可以在程序的编译器确定,所以可以对其进行优化;反观引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
- 所以上面的 s3 之所以会等于 s5 ,是由于常量折叠造成的。
- 由于常量折叠的优化,使得原来的
String s3 = "ze" + "ro";
编译器会给你优化成String s3 = "zero";
String s1 = "ze";
String s2 = "ro";
String s3 = "ze" + "ro"; //由于常量折叠等同于s5,最后分配在字符串常量池中的对象
String s4 = s1 + s2; //由 StringBuilder 的 toString() 最终生成,上面有提到,分配在堆内存中
String s5 = "zero"; //字符串常量,分配在字符串常量池中
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s4 == s5);//false
- 再来做几个练习题吧,我就不解释了,大家练练手就好。
final String s1 = "ze";
final String s2 = "ro";
String s3 = "ze" + "ro"; // 常量折叠,常量池中的对象
String s4 = s1 + s2; // 常量折叠,常量池中的对象
System.out.println(s3 == s4); // true
- 大家记住一个点,就是
如果编译器可以在运行前就确定值的话,就可以时候用常量折叠,否则不行
。
小结
- 创建字符串常量,也就是直接用""创建的字符串,是分配在字符串常量池。
- 使用 new 关键字,通过 String 类的构造器创建的字符串对象,是分配到堆内存的。
- intern() 方法可以保证在字符串常量池中会有一个与当前字符串值一样的字符串对象存在。
- 当字符串在做"+"运算的时候是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后,调用 toString() 得到一个 String 对象 。
- 常量折叠是 Javac 编译器会对源代码做的极少量优化措施之一,会把常量表达式的值求出来作为常量嵌在最终生成的代码中。