目录
String基本特性
- 使用""表示,String str = "" 字面量形式,String str = new String("") 。
- final 修饰,不可继承
- 实现了 java.io.Serializable。天然可以跨进程传输
- 实现了 Comparable<String> 。可排序
- 实现了 CharSequence。描述字符串结构的接口。
- String代表了不可变的字符序列。
- 字符串常量池不会存储相同内容的字符串
char 改为 byte
1.8之前使用char数组存储,1.9之后使用的是byte数组。原因见http://openjdk.java.net/jeps/254 的Motivation和Description。这里要了解一下decode。
Motivation
The current implementation of the String
class stores characters in a char
array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String
objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char
arrays of such String
objects is going unused.
Description
We propose to change the internal representation of the String
class from a UTF-16 char
array to a byte
array plus an encoding-flag field. The new String
class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.
之前String存储字符是用char型数组,而char型数组是占两个字节(16位)的。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分。而且,大多数字符串对象只包含拉丁字符。拉丁字符包括ASCII码和IOS8859-1,IOS8859-1是欧洲码范围0-255,ASCII的范围是0-127。所以这种字符只需要2^8=256,一个字节就能存下。这样的话,使用这些字符编码,会有一半空间浪费。将String类的内部表示形式从UTF-16字符数组更改为byte数组加上编码标志字段。
同时,AbstractStringBuilder
, StringBuilder
, 和 StringBuffer也做了相应修改:
String-related classes such as AbstractStringBuilder
, StringBuilder
, and StringBuffer
will be updated to use the same representation, as will the HotSpot VM's intrinsic string operations.
String不可变性案例
String s1 = "aaaa"
String s2 = "aaaa"
// s1 和 s2 都是从常量池获取的同一个实例
s1 = s1 + "bbb";
// 将常量池中的"aaaa"和常量池中的"bbb"拼接创建一个新的实例,将其引用给到s1
s1.relace('a', 'b')
// 也是创建一个新的对象 将其引用返回,"aaaa"并没有改变
private void change(String str, char ch[]) {
str = "test ok";
char[0] = "b";
}
public void test () {
str = "aaaa";
char[] ch = {'t','e','s','t'};
change(str, ch);
System.out.println(str);// aaaa
System.out.println(ch); // best
}
字符串常量池不会存储相同内容的字符
String的String Pool 是一个固定大小的Hashtable,默认大小为1009。如果放进String Pool的的String非常多,机会导致hash冲突,从而导致链表变长,链表过长,会直接导致调用String.intern时性能下降。
使用-XX:StringTableSize 设置StringTable的长度。
jdk6中StringTable的大小固定的就是1009,如果String过多导致效率下降,StringTableSize设置多少都可以,没有要求。
jdk7中,默认长度时60013,StringTableSize设置多少都可以,没有要求。
jdk8后,1009是要求可以设置的最小值。
String的内存分配
在java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中更快,更节省内存,都提供了一种常量池概念。
常量池就类似一个Java系统级别提供的缓存,8种基本数据类型都是系统协调的。String类型的常量池比较特殊,它主要使用方法有两种。
- 直接使用双引号申明出来的字面量而创建的String对象会直接存在常量池中
- 可以通过intern()方法将运行时创建的String对象加入常量池
历史:
- java 6 及以前,字符串常量池存放在永久代。
- java 7 中, 将字符串常量池的位置调整到java堆中。这样调优的时候只需要调整堆就行了。使用intern方法也利于我们管理。
- java 8 元空间,String对象存在堆中,
将String常量的引用放在元空间。static的String引用和Class对象一起存在堆中。
为什么将StringTable调整到堆中
- 整合Jrockit的需要
- permSize默认比较小,大小也不太好调整,容易OOM
- 将对象统一放置堆中,好管理,尤其是使用intern方法的时候。
- 永久代垃圾回收频率低
String的基本操作
System.out.println("111")
System.out.println("111")
这样只会创建一个“111”的String对象在常量池。java规范中要求完全相同的字符串字面量,应该时具有相同的Unicode字符序列,并且必须指向同一个String实例。
字符串拼接操作
常量池不会存在相同内容常量
常量与常量的拼接,其结果在常量池,原理时编译器优化。
String s1 = "a" + "b" + "c";
// 这种操作编译期间就会优化成"abc"
String s2 = "abc";
// s1 == s2
拼接表达式中,只要有一个是变量,其结果就不在常量池,就是说该对象在堆中创建,但是不被StringTable所管理,可以理解为常量池以外的堆空间,这种拼接,其实使用的是StringBuilder。
String s1 = "aaa";
String s2 = "bbb";
String s3 = "aaa" + "bbb";
String s4 = "aaa" + s2;
System.out.println(s4 == s3);// false "aaa" + s2;在非常量池堆空间,"aaa" + "bbb"在常量池堆空间。
拼接原理
1、存在 字面量拼接或者final(因为修饰为final,其赋值在编译期间就已经完成)修饰引用的拼接,在编译期间就已经完成拼接成字面量,这种拼接后的字符串存在常量池。如下案例。(开发中能使用final建议就加final)
public void test3() {
final String s1 = "aaa";
final String s2 = "bbb";
String s3 = s1 + s2;
String s4 = "aaa" + "bbb";
System.out.println(s3 == s4);
// true
}
/**
Code:
stack=3, locals=5, args_size=1
0: ldc #2 // String aaa
2: astore_1
3: ldc #3 // String bbb
5: astore_2
6: ldc #8 // String aaabbb
8: astore_3
9: ldc #8 // String aaabbb
11: astore 4
13: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_3
17: aload 4
19: if_acmpne 26
22: iconst_1
23: goto 27
26: iconst_0
27: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
30: return
*/
2、存在变量引用(除final修饰外,final修饰就是指常量引用)拼接时,拼接时,其实是编译为class文件后,使用StringBuilder进行append。最后toString()返回。
public void test1() {
String s1 = "aaa";
String s2 = "bbb";
String s3 = s1 + s2;
}
/**
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // String aaa
2: astore_1
3: ldc #3 // String bbb
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: return
*/
补充
- 这里的拼接工具在jdk5.0之前用的是StringBuffer,为了提升效率,在5.0之后用的是StringBuilder。
- 如下图。如果我们在循环体中字符串拼接,直接使用 ”+“ 号拼接,编译器编译之后的结果是,在每一次循环都会创建一个StringBuilder进行拼接,并且创建一个String对象返回。从而增加的对象的创建(我们知道new 是需要加锁的),并且还会降低GC效率。因此,我们在循环体中拼接字符串,必须在循环体外显示地new 一个StringBuilder,并在循环体中显式的进行append。我循环10万次,二者差距600倍(7:4014)
- 如果我们拼接字符串时需要考虑线程安全问题,也是需要显示地new StringBuffer进行append。
- StringBuilder/StringBuffer的char[]的capacity 默认等于16,如果我们大致可以确定其大小,最好使用有参构造器。
public void test2() {
String s1 = "aaa";
String s2 = "bbb";
for (int i = 0; i < 100; i++) {
s1 = s1 + s2;
}
}
/**
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // String aaa
2: astore_1
3: ldc #3 // String bbb
5: astore_2
6: iconst_0
7: istore_3
8: iload_3
9: bipush 100
11: if_icmpge 39
14: new #4 // class java/lang/StringBuilder
17: dup
18: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
21: aload_1
22: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: aload_2
26: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_1
33: iinc 3, 1
36: goto 8
39: return
*/
intern使用
是一个native方法
- 过程:判断当前字符串的字符序列在StringTable中是否已经存在(使用equals方法),如果已经存在则,返回存在的已存在对象的引用,如果不存在,则在常量池中添加一个新的引用,该引用指向该对象(这里注意不是创建一个新的对象,但是在jdk1.6会在永久代创建一个新的对象,然后返回这个引用),并将其引用返回。
案例
//在堆中创建对象"aaabbb"
String s1 = new String(new String("aaa") + new String("bbb"));
// StringTable中新增一个ref, 该ref == s8, 然后将该ref返回。最后 s2 == ref
String s2 = s1.intern();
// true
System.out.println(s1 == s2);
// 通过字面量形式在堆中创建一个"ccc"对象,将其引用给到StringTable
String s3 = "ccc";
// 在堆空间创建一个对象,该对象的char[] 和之前 的 "ccc" 一样, 但是地址不同
String s4 = new String(s3);
// 发现StringTable中已经有了"ccc", 将其引用返回,但是,并没有返回给s4,因此s4 != s3
s4.intern();
// false
System.out.println(s3 == s4);
String s6 = new String(new String("ddd") + new String("eee"));
s6.intern();
// 将StringTable 中 ref的引用给到s5
String s5 = "dddeee";
// true
System.out.println(s5 == s6);
图解
要理解原理,要先理解java的引用传递,StringTable中存储的不过是字符串的地址。字符串的对象实际分散在堆空间的不同地方。
s1最初指向"aaabbb", s1.intern()后,stringtable[i] == s1(意思是,stringtable的第i位和s1一样都指向”aaabbbb“),然后 s2 = stringtable[i] ,因此s1、s2 和stringtable[i]都是指向”aaabbbb“。
s3 一开始就是和常量池中的stringtable[j] 相等,由于”ccc“ 已经被stringtable[j]维护,s4.intern(),仅仅返回stringtable[j],但是这个stringtable[j]并没有再次赋给s4,因此 s3 和 stringtable[j] 指向”ccc“ 而 s4还是指向 另外一个”ccc“。
注意:
上面的解释是基于jdk7/8。jdk1.6不一样,jdk1.6的intern会在永久代new一个新对象。所以上面案例的在1.6中全部都是false
总结
对于可能存在大量的重复字符串,使用intern可以节省内存空间
public class StingIntern {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) throws InterruptedException {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
arr[i] = String.valueOf(data[i % data.length]);
//arr[i] = String.valueOf(data[i % data.length]).intern();
}
long end = System.currentTimeMillis();
System.out.println("花费时间:" + (end - start));
Thread.sleep(1000000000L);
}
}
StringTable的垃圾回收
虽然使用intern可以去重复,节省空姐,但是,String对象还是会创建,创建之后通过equals比较比较之后,将常量池中的引用地址返回,最后本次创建的String如果无引用指向就会被GC。
G1的String去重
http://openjdk.java.net/jeps/192
动机
当前,许多大型Java应用程序已成为内存瓶颈。测量表明,在这些类型的应用程序中,大约25%的Java堆活动数据集被String
对象占用。此外,这些String
对象中大约有一半是重复项,其中重复项 string1.equals(string2)
是正确的。String
从本质上讲,在堆上具有重复的对象只是浪费内存。该项目将String
在G1垃圾收集器中实现自动和连续重复数据删除,以避免浪费内存并减少内存占用。
实现
当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选(达到一定年龄)要去重的String对象。
如果是,把这个对象的一个引用插入到对象等待后续处理。一个去重线程在后台运行,处理这个队列的一个元素意味着从队列删除这个元素,然后尝试重用它引用的String对象。
使用一个hashtable来记录所有的被String对象使用的不重复的char数组,当去重的时候会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
如果存在,String对象会被调整引用那个数组,释放对原来数组的引用,最后被垃圾回收器回收。
如果查找失败,char数组会被插入到hashtable。这样以后就可以共享这个数组了。
命令行选项:
- -XX:+UseStringDeduplication 开启String去重,默认不开启,需要手动开启
- -XX:+PrintStringDeduplicationStatics 打印详细去重统计信息
- -XX:StringDeduplicationAgeThreshold 达到这个年龄的String对象被认为是去重的候选对象。
JVM对String的优化:
- char 改为byte。
- 对重复String进行去重。