之前有面试官也问过,但是在平时并没有感觉出三者有什么太大的不同,但是最近在读《java特种兵》一书,感觉三者的文章真的很多。
一般认为在字符串的拼接时三者的顺序是:StringBuilder>StringBuffer>String。
StringBuilder是线程不安全的,StringBuffer线程安全,多线程下会锁定对象,时间略慢,String在字符串操作时会产生新的字符串,最慢。
但是,当字符串的凭借全部是常量时,这时候string的优势是最大的,通过编译器优化,字符串常量的+自动优化成一个字符串常量,此时最快,但是当String的并接中出现了变量,String的优势就没有了。
我做了一个实验,代码如下,做1w次的字符串并接
long time = System.currentTimeMillis();
StringBuffer c = new StringBuffer("a");
for (int i = 0; i < 10000; i++) {
c.append(i+"喝酒啊哈哈哈哈哈哈0");
}
System.out.println("StringBuffer 的时间:"+(System.currentTimeMillis()-time));
time = System.currentTimeMillis();
c=null;//帮助GC回收,避免长时间占用堆,排除下面oom和这有关系。
StringBuilder a = new StringBuilder("a");
for (int i = 0; i < 10000; i++) {
a.append(i+"喝酒啊哈哈哈哈哈哈0");
}
System.out.println("StringBuilder 的时间:"+(System.currentTimeMillis()-time));
a = null;
time = System.currentTimeMillis();
String b = "a";
for (int i = 0; i < 10000; i++) {
b+=i+"喝酒啊哈哈哈哈哈哈0";
}
System.out.println("String 的时间:"+(System.currentTimeMillis()-time));
最后的输出日志:
StringBuffer 的时间:25
StringBuilder 的时间:23
[GC [PSYoungGen: 15336K->2136K(18944K)] 15336K->3672K(61952K), 0.0040222 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 18212K->600K(35328K)] 19748K->11360K(78336K), 0.0057828 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
[GC [PSYoungGen: 25467K->536K(35328K)] 36227K->29730K(78336K), 0.0082693 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC [PSYoungGen: 536K->0K(35328K)] [ParOldGen: 29194K->18910K(61952K)] 29730K->18910K(97280K) [PSPermGen: 2718K->2717K(21504K)], 0.0166769 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [PSYoungGen: 24768K->32K(56320K)] 559817K->547369K(745472K), 0.0104553 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 32K->32K(66048K)] 547369K->547369K(755200K), 0.0040113 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(66048K)] [ParOldGen: 547337K->197100K(252928K)] 547369K->197100K(318976K) [PSPermGen: 2717K->2717K(21504K)], 0.0821627 secs] [Times: user=0.11 sys=0.03, real=0.08 secs]
[GC [PSYoungGen: 243K->32K(103936K)] 590592K->590380K(793088K), 0.0050491 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 32K->32K(103936K)] 590380K->590380K(793088K), 0.0046632 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
[Full GC [PSYoungGen: 32K->0K(103936K)] [ParOldGen: 590348K->197100K(262656K)] 590380K->197100K(366592K) [PSPermGen: 2717K->2717K(21504K)], 0.1007265 secs] [Times: user=0.22 sys=0.03, real=0.10 secs]
[GC [PSYoungGen: 0K->0K(173568K)] 393724K->393724K(862720K), 0.0037389 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 0K->0K(183296K)] 393724K->393724K(872448K), 0.0032845 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 0K->0K(183296K)] [ParOldGen: 393724K->393724K(467968K)] 393724K->393724K(651264K) [PSPermGen: 2717K->2717K(21504K)], 0.0060832 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 0K->0K(255488K)] 393724K->393724K(944640K), 0.0020710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 0K->0K(255488K)] [ParOldGen: 393724K->393713K(475136K)] 393724K->393713K(730624K) [PSPermGen: 2717K->2717K(21504K)], 0.1100342 secs] [Times: user=0.34 sys=0.00, real=0.11 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at com.Demo.main(Demo.java:50)
Heap
PSYoungGen total 255488K, used 6040K [0x00000000eaf00000, 0x00000000fb780000, 0x0000000100000000)
eden space 253440K, 2% used [0x00000000eaf00000,0x00000000eb4e6098,0x00000000fa680000)
from space 2048K, 0% used [0x00000000fa880000,0x00000000fa880000,0x00000000faa80000)
to space 2048K, 0% used [0x00000000fa680000,0x00000000fa680000,0x00000000fa880000)
ParOldGen total 689152K, used 393713K [0x00000000c0e00000, 0x00000000eaf00000, 0x00000000eaf00000)
object space 689152K, 57% used [0x00000000c0e00000,0x00000000d8e7c530,0x00000000eaf00000)
PSPermGen total 21504K, used 2748K [0x00000000bbc00000, 0x00000000bd100000, 0x00000000c0e00000)
object space 21504K, 12% used [0x00000000bbc00000,0x00000000bbeaf328,0x00000000bd100000)
String最后干脆直接oom了,String的慢为什么慢,验证一下是不是因为GC的频繁活动导致的。StringBuilder在单线程下速度和StringBuilder相差不大,不需要同步。上面可以看出String在大量字符串并接的时候GC活动频繁。调大堆内存后在尝试,结果是GC的次数越少越快,尤其是fullGC的次数
把上面的循环数量级缩小到100以内日志输出:
String 的时间:5
StringBuilder 的时间:2
StringBuffer 的时间:3
long time = System.currentTimeMillis();
String b = "a";
for (int i = 0; i < 400; i++) {
b+=i+"喝酒啊哈哈哈哈哈哈0";
}
System.out.println("String 的时间:"+(System.currentTimeMillis()-time));
b=null;
time = System.currentTimeMillis();
String a = "a";
for (int i = 0; i < 400; i++) {
StringBuilder tmp = new StringBuilder();
tmp.append(a).append(i+"喝酒啊哈哈哈哈哈哈0");
a=tmp.toString();
}
System.out.println("String 的时间:"+(System.currentTimeMillis()-time));
时间主要是花费在了对象的创建上,日志输出:
String 的时间:4
String 的时间:6
Heap
PSYoungGen total 18944K, used 13440K [0x00000000eaf00000, 0x00000000ec400000, 0x0000000100000000)
eden space 16384K, 82% used [0x00000000eaf00000,0x00000000ebc20240,0x00000000ebf00000)
from space 2560K, 0% used [0x00000000ec180000,0x00000000ec180000,0x00000000ec400000)
to space 2560K, 0% used [0x00000000ebf00000,0x00000000ebf00000,0x00000000ec180000)
ParOldGen total 43008K, used 0K [0x00000000c0e00000, 0x00000000c3800000, 0x00000000eaf00000)
object space 43008K, 0% used [0x00000000c0e00000,0x00000000c0e00000,0x00000000c3800000)
PSPermGen total 21504K, used 2716K [0x00000000bbc00000, 0x00000000bd100000, 0x00000000c0e00000)
object space 21504K, 12% used [0x00000000bbc00000,0x00000000bbea72b8,0x00000000bd100000)
理论上分析:
String在做字符串+操作时,会优化成StringBuilder,当String长度超过StringBuilder的初始长度16时,此时StringBuilder会扩容,如果append的字符串长度小于32,则StringBuilder扩容为32,否则扩容为String的长度。随着字符串+运算,超过这个是肯定的,这样每一次都会导致StringBuilder扩容,但是下次在+操作时又要重新申请一堆内存,不但新申请的占用了堆内存,而且原有字符串在扩容时还存在,这样更加消耗堆内存,垃圾随着循环次数的增加不断增加,引起GC的频繁回收,增加了操作的时间。
StringBuilder和StringBuffer在做append操作时应优先append长的字符串,避免每次append时的扩容操作的发生