理解 String、StringBuilder、StringBuffer
我们上面说到,使用 +
连接符时,JVM 会隐式创建 StringBuilder 对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。如下这段代码
String s = "aaaa"; for (int i = 0; i < 100000; i++) { s += "bbb"; }
这是一段很普通的代码,只不过对字符串 s 进行了 + 操作,我们通过反编译代码来看一下。
// 经过反编译后 String s = "aaa"; for(int i = 0; i < 10000; i++) { s = (new StringBuilder()).append(s).append("bbb").toString(); }
你能看出来需要注意的地方了吗?在每次进行循环时,都会创建一个 StringBuilder
对象,每次都会把一个新的字符串元素 bbb
拼接到 aaa
的后面,所以,执行几次后的结果如下
每次都会创建一个 StringBuilder ,并把引用赋给 StringBuilder 对象,因此每个 StringBuilder 对象都是强引用
, 这样在创建完毕后,内存中就会多了很多 StringBuilder 的无用对象。了解更多关于引用的知识,请看
这样由于大量 StringBuilder 创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个 StringBuilder 对象调用 append()
方法手动拼接。
例如
StringBuilder builder = new StringBuilder("aaa"); for (int i = 0; i < 10000; i++) { builder.append("bbb"); } builder.toString();
这段代码中,只会创建一个 builder 对象,每次循环都会使用这个 builder 对象进行拼接,因此提高了拼接效率。
从设计角度理解
我们前面说过,String 类是典型的 Immutable
不可变类实现,保证了线程安全性,所有对 String 字符串的修改都会构造出一个新的 String 对象,由于 String 的不可变性,不可变对象在拷贝时不需要额外的复制数据。
String 在 JDK1.6 之后提供了 intern()
方法,intern 方法是一个 native
方法,它底层由 C/C++ 实现,intern 方法的目的就是为了把字符串缓存起来,在 JDK1.6 中却不推荐使用 intern 方法,因为 JDK1.6 把方法区放到了永久代(Java 堆的一部分),永久代的空间是有限的,除了 Fullgc
外,其他收集并不会释放永久代的存储空间。JDK1.7 将字符串常量池移到了堆内存
中,
下面我们来看一段代码,来认识一下 intern
方法
public static void main(String[] args) { String a = new String("ab"); String b = new String("ab"); String c = "ab"; String d = "a"; String e = new String("b"); String f = d + e; System.out.println(a.intern() == b); System.out.println(a.intern() == b.intern()); System.out.println(a.intern() == c); System.out.println(a.intern() == f); }
上述的执行结果是什么呢?我们先把答案贴出来,以防心急的同学想急于看到结果,他们的答案是
false true true false
和你预想的一样吗?为什么会这样呢?我们先来看一下 intern 方法的官方解释
这里你需要知道 JVM 的内存模型
虚拟机栈
: Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈种创建一个栈帧(stack frame)
。本地方法栈
: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用native
关键字修饰的方法所存储的区域程序计数器
:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。方法区
:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。堆
:堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上运行时常量池
:运行时常量池又被称为Runtime Constant Pool
,这块区域是方法区的一部分,它的名字非常有意思,它并不要求常量一定只有在编译期才能产生,也就是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String 的 intern 方法就是一个典型的例子。
在 JDK 1.6 及之前的版本中,常量池是分配在方法区中永久代(Parmanent Generation)
内的,而永久代和 Java 堆是两个完全分开的区域。如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回常量池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。
一些人把方法区称为永久代,这种说法不准确,仅仅是 Hotspot 虚拟机设计团队选择使用永久代来实现方法区而已。
从JDK 1.7开始去永久代
,字符串常量池已经被转移至 Java 堆中,开发人员也对 intern 方法做了一些修改。因为字符串常量池和 new 的对象都存于 Java 堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。
所以我们对上面的结论进行分析
String a = new String("ab"); String b = new String("ab"); System.out.println(a.intern() == b);
输出什么?false,为什么呢?画一张图你就明白了(图画的有些问题,栈应该是后入先出,所以 b 应该在 a 上面,不过不影响效果)
a.intern 返回的是常量池中的 ab,而 b 是直接返回的是堆中的 ab。地址不一样,肯定输出 false
所以第二个
System.out.println(a.intern() == b.intern());
也就没问题了吧,它们都返回的是字符串常量池中的 ab,地址相同,所以输出 true
然后来看第三个
System.out.println(a.intern() == c);
图示如下
a 不会变,因为常量池中已经有了 ab ,所以 c 不会再创建一个 ab 字符串,这是编译器做的优化,为了提高效率。
下面来看最后一个
System.out.println(a.intern() == f);