首先我们要了解到,字符串常量是存储在常量池中的。在JDK1.7以前,常量池处于方法区当中。此时hotspot虚拟机对方法区的实现为永久代。对于永久代,大家需要了解他的一个特性,那就是GC不会对永久代内的数据进行垃圾回收。
String类型很重要的一个特性就是,字符串是不可变的,他们的值在创建后无法更改。看下面两行代码:
String s = "aaa";
s = "abc";
当执行第一行代码时,栈内存入栈了一个引用s,常量池存储了一个字符串常量“aaa”,s指向了“aaa”的地址。当执行第二行代码时,并不是"aaa"的内容改变成了“abc”,而是常量池中又存储了字符串常量“abc”,s指向的地址改为了“abc”的地址。“aaa”无引用指向,成为系统垃圾。
根据上述永久代和String类型的特性,当我们使用“+”拼接字符串时就会发生浪费空间的问题。详情请看下面一段代码:
String text1 = "abc";
String text2 = "def";
String text3 = "gh";
text1 = text1+text2+text3;
System.out.println(text1);
此段代码看上去没有任何问题,运行后也输出了“abcdefgh”。那究竟为何会浪费空间呢?但让我们来分析一下此时内存中的变化。
首先,前三条代码执行后,栈内存中已经入栈了text1,text2,text3三个变量,常量池中也存储了三个字符串常量,栈中的三个变量分别指向了所对应的地址。如下图:
当执行第四行代码,字符串进行了拼接。此时注意,text1+text2+text3看起来是一个式子,但在执行过程中,内存中会新增加两个字符串常量。第一个是text1和text2拼接所生成的字符串(“abcdef”),第二个是第一个生成的字符串和text3字符串拼接后所生成的字符串(“abcdefgn”)。拼接完成后text1所指向的地址将会修改成最后生成的字符串地址,此时常量池中则出现了两个没有任何引用指向的常量,系统垃圾“abc”和“abcdef”。如下图:
而根据永久代的特性,这两个垃圾将不会被GC回收,成为“永久垃圾”!
此处看上去好像只是内存中多出了两个字符串而已,但如果业务规模大了起来,如果这种拼接是发生在一个大规模的循环体中,那么产生的垃圾可就不只是两个而已了,将会造成系统空间的大量浪费。
通常我们创建StringBuffer/StringBuilder对象方式如下:(以StringBuffer为例)
StringBuffer s1 = new StringBuffer("abcd");
当new关键字一出现,我们就应当知道,此时要创建一个对象了。而在Java中new出来的对象的实例存放在堆内存中,对象的引用存放在栈内存中。如下图:
与String类型不同的是,StringBuffer/StringBuilder是可变字符串,他们的值可以发生变化。请看以下代码:
StringBuffer s1 = new StringBuffer("abcd");
s1.append("efg");
第一行代码执行后内存已经分析过,如上图。当执行第二行代码时,append()方法的作用就是在末尾拼接字符串,由于StringBuffer是可变字符串,所以拼接即发生在当前StringBuffer的实例上,无需再新建实例。即改变了原实例内的字符串内容,s1依然指向原地址。内存变化如下:
以上例子均使用StringBuffer实现,StringBuilder进行拼接字符串操作时与StingBuffer相同。由此可见,使用StringBuffer/StringBuilder拼接字符串不会造成空间的浪费。
(1)线程安全
分析源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer:线程安全,StringBuilder:线程不安全
因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有。
(2)缓冲区
StringBuffer 代码片段:
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder 代码片段:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以看出,StringBuffer
每次获取 toString
都会直接使用缓存区的 toStringCache
值来构造一个字符串。而 StringBuilder
则每次都需要复制一次字符数组,再构造一个字符串。所以,缓存冲这也是对 StringBuffer
的一个优化吧,不过 StringBuffer
的这个toString
方法仍然是同步的。
(3)性能
StringBuffer是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder的性能要远大于StringBuffer。
StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,如果是单线程场合 StringBuilder 更适合。
参考:https://blog.csdn.net/qq_41711633/article/details/115793928、https://blog.csdn.net/weixin_45393094/article/details/104526603