内部实现
String 底层实现是用final修饰的,典型的Immutable类,java9之前内部的实际存储结构是final修饰的char[],java9之后改为了final修饰的byte[],目的也很明显,String几乎是用途最广泛的类型之一,如果能减少他的实际存储大小,那就能大大的缩减内存中的使用。(缩减算法可能类似于霍夫曼编码,还有待考证)
字符拼接
由于String是不可变的,所以其频繁的修改和变动会带来大量的中间对象,StringBuilder和StringBuffer就是为了处理频繁拼接和修改的,原理是维护一个char数组,修改的过程只修改该数组,直到最后toString的时候再生成String对象,这样可以避免很多中间临时对象给gc造成负担。二者的区别也就是前者非线程安全,后者线程安全(也就是在所有修改方法上添加了synchronize关键字而已)
那我们平时的拼接就要尽量避免普通的加号吗?完全没必要,jvm已经很智能了,可以替我们优化
String str = "abc";
String str1 = str + "d" + "e";
这是一个普通的字符拼接,通过javap -v [类名]命令后 可以看到(无关的因素已隐去):
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String de
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
....
可以很清楚的看到main方法中,偏移量为7的那行,jvm很智能的替我们添加了StringBuilder的实例化
字符串缓存
如果你曾经通过 jmap -histo:live [processId]看存活的对象,会发现
1: 71023 7346320 [C
2: 25216 2219008 java.lang.reflect.Method
3: 65094 2083008 java.util.concurrent.ConcurrentHashMap$Node
4: 8639 1881208 [B
5: 70823 1699752 java.lang.String
6: 14760 1630288 java.lang.Class
7: 8695 1064200 [I
8: 14997 977112 [Ljava.lang.Object;
9: 19164 919872 org.aspectj.weaver.reflect.ShadowMatchImpl
10: 20583 823320 java.util.LinkedHashMap$Entry
....
字符串对象和map对象常年名列前矛,而后者管理不善更是gc泄漏的主要原因之一...
java6之后String提供了intern方法来对其进行缓存,减少String实例,例如:
String str = new String("a") + new String("bc");
String str1 = "abc";
System.out.println(str == str1);
-----------------------
false
String str = new String("a") + new String("bc");
str.intern();
String str2 = "abc";
System.out.println(str == str2);
-----------------------
true
Java 6的时候被缓存的字符串存储于永久带中,这个空间有限且几乎不被gc照顾,很容易发生OOM,后续版本中,这个参数被放到了堆中,这样就解决了这个问题。