文章目录
1. String
1.1 String的基本特性
String
:字符串,使用一对""引起来表示String
声明为final
的,不可被继承String
实现了Serializable
接口:表示字符串是支持序列化的。String
实现了Comparable
接口:表示String
可以比较大小String
在JDK8
及以前内部定义了final char[] value
用于存储字符串数据。JDK9
时改为byte[]
。
1.2 String 的存储结构变化
String在JDK9中存储结构变更:官方网站说明:JEP 254: Compact Strings (java.net)
对官方中的内容说明进行翻译:
动机
目前String
类的实现将字符存储在一个char
数组中,每个字符使用两个字节(16位)。从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分,此外,大多数字符串对象只包含Latin-1字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用。
说明
我们建议将String
类的内部表示方法从UTF-16
字符数组改为字节数组加编码标志域。新的String类将根据字符串的内容,以ISO-8859-1/Latin-1
(每个字符一个字节)或UTF-16
(每个字符两个字节)的方式存储字符编码。编码标志将表明使用的是哪种编码。
与字符串相关的类,如AbstractStringBuilder
、StringBuilder
和StringBuffer
将被更新以使用相同的表示方法,HotSpot VM
的内在字符串操作也是如此
这纯粹是一个实现上的变化,对现有的公共接口没有变化。目前没有计划增加任何新的公共API或其他接口。
迄今为止所做的原型设计工作证实了内存占用的预期减少,GC活动的大幅减少,以及在某些角落情况下的轻微性能倒退。
结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
}
1.3 基本特性
String:代表不可变的字符序列。简称:不可变性。
● 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
● 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value
进行赋值。
● 当调用string
的replace()
方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value
进行赋值。
通过字面量的方式(区别于new
)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
字符串常量池是不会存储相同内容的字符串的
String
的String Pool
是一个固定大小的Hashtable,默认值大小长度是1009
。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern
时性能会大幅下降。
使用-XX:StringTablesize
可设置StringTable
的长度
● 在JDK6中StringTable
是固定的,就是1009
的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize
设置没有要求
● 在JDK7中,StringTable的长度默认值是60013
,StringTablesize
设置没有要求
● 在JDK8中,设置StringTable
长度的话,1009是可以设置的最小值
2. String的内存分配
在Java语言中有8种基本的数据类型和比较特殊的数据类型String。这些类型为了使他们运行的更加的快速,更节省内存空间,都提供了一种叫做常量池的概念。
但是,8种基本数据类型都是系统协调的,String数据类型的常量池比较特殊,她的使用方法有两种.
- 如果是直接使用双引号声明出来的String对象的会直接存储在常量池中。
- 如果不是双引号声明的String对象可以通过String提供的intern()方法。
下面是StringTable的在JVM内存结构划分中的位置:
JDK1.6及之前 | 有永久代,字符串常量池、静态变量存放在永久代上 |
---|---|
JDK1.7 | 有永久代,但已经逐步”去永久代“,字符串常量池、静态变量以及保存在堆中了。 |
JDK1.8及之后 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 |
具体的文章内容看:JVM方法区在JDK6、JDK7、JDK8变化
StringTable为什么要调整地方?
在JDK 7中,内部字符串不再分配在Java堆的永久代中,而是分配在Java堆的主要部分(称为年轻代和老年代),与应用程序创建的其他对象一起。这种变化将导致更多的数据驻留在主Java堆中,而更少的数据在永久代中,因此可能需要调整堆的大小。大多数应用程序将看到由于这一变化而导致的堆使用的相对较小的差异,但加载许多类或大量使用String.intern()方法的大型应用程序将看到更明显的差异
3. String基本操作
@Test
public void test1() {
System.out.print1n("1"); //2321
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10"); //2330
System.out.println("1"); //2321
System.out.println("2"); //2322
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.print1n("6");
System.out.print1n("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2330
}
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
小结:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象
我们对代码来进行举例说明:
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
假设有上面的代码:通过: javap -v Demo1_22
进行反编译。上面代码中,通过字符串字面量的方式创建了几个 String
。对于变量s1, s2, s3
,都知道被放在了栈中,后面的字符串存储在哪里?
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
通常这里的#2
就是a,当类加载的时候,常量池中的信息会加载到运行时常量池中,此时的a,b,ab
都还是符号,没有变成Java
对象。当运行此方法,执行到对应的代码时,才会将符号a变成“a”字符串对象,并将对象放入StringTable
中。 需要注意的是,普通的Java
对象在类加载的时候就会生成并放入堆中,而这种方式生成的String
不同,只有当执行到新建String
的代码时才会生成字符串对象。
StringTable
是一个哈希表,长度固定,“a”
就是哈希表的key
。
一开始的时候,会根据“a”
到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]
。
执行到指令ldc #3
时,会和上面一样,生成一个“b”
对象并放入串池中,串池变为[“a”, “b”]
。
后面会生成“ab”
对象并放入串池中。串池变为[“a”, “b”, “ab”]
。
4. 字符串变量拼接
● 常量与常量的拼接结果在常量池,原理是编译期优化
● 常量池中不会存在相同内容的变量
● 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
● 如果拼接的结果调用intern()
方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
在熟悉了上面关于s = ''这种方式的具体原理之后: 看看关于str = s1 + s2
的时候
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
}
}
对编译之后的代码进行反编译得到:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return
有关于s4 = s1 + s2
前面的指令我们已经很熟悉,观察行号为9
的指令,这里是个new
。这就说明s4
的创建方式和s1、s2、s3
不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。
观察行号9
的指令后面的注释,可以知道这里是new
了一个StringBuilder
对象。
接着看17
,21
,可以发现“s1 + s2”
的方式是通过StringBuilder
对象调用append方法实现的。
最后看24
,最后是调用了toString
方法生成了新的字符串对象。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
这里将StringBuilder
中的值在new
了一个string
的对象。
总结:以上分析就想要说明:即当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。 最后我们看输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。
下面看几段代码就可以很好区分了,具体的还是要自己编译看看背后的逻辑:
public static void test1() {
// 都是常量,前端编译期会进行代码优化
// 通过idea直接看对应的反编译的class文件,会显示 String s1 = "abc"; 说明做了代码优化
String s1 = "a" + "b" + "c";
String s2 = "abc";
// true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值
System.out.println(s1 == s2);
}
[举例子2]
public static void test5() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); // true 编译期优化
System.out.println(s3 == s5); // false s1是变量,不能编译期优化
System.out.println(s3 == s6); // false s2是变量,不能编译期优化
System.out.println(s3 == s7); // false s1、s2都是变量
System.out.println(s5 == s6); // false s5、s6 不同的对象实例
System.out.println(s5 == s7); // false s5、s7 不同的对象实例
System.out.println(s6 == s7); // false s6、s7 不同的对象实例
String s8 = s6.intern();
System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
}
[举例子3]
public void test6(){
String s0 = "beijing";
String s1 = "bei";
String s2 = "jing";
String s3 = s1 + s2;
System.out.println(s0 == s3); // false s3指向对象实例,s0指向字符串常量池中的"beijing"
String s7 = "shanxi";
final String s4 = "shan";
final String s5 = "xi";
String s6 = s4 + s5;
System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期就能确定s6的值了
}
● 不使用final修饰,即为变量。如s3行的s1和s2,会通过new StringBuilder进行拼接
● 使用final修饰,即为常量。会在编译器进行代码优化。在实际开发中,能够使用final的,尽量使用
5. intern()的使用
String中的intern的使用
对于下面这段代码进行分析:
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s1 == "ab"); // true
System.out.println(s == "ab"); // false
首先来进行反编译
Code:
stack=4, locals=3, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String a
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
对反编译的结果来进行解读:
开始一看到+
号,首先new
一个StringBuilder
的对象
看到new String("a")
之后,先将a
放入到StringTable
中,之后在堆中创一个String
的对象。两个在通过append
来进行拼接,通过StringBuilder
的toString()
方法获得一个新的String
对象。
此时StringTable
中有元素a, b, c
,堆中新增了元素 new String('a'), new String('b'), new String('ab'), new StringBuilder()
对于intern
方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”
对象并返回,并且会让s
指向串池中的“ab”
对象。
但是,注意的是:上面是JDK1.7
之后的做法,``JDK1.6,当一个
String调用
intern`方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回StringTable中刚加入的对象。并不会将s指向串池中的对象(如果没有话)。
面试题
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d"); // new String("cd");
x2.intern(); // 1
String x1 = "cd"; // 2
// 如果是jdk1.6的话,会有什么样的不同
System.out.println(x1 == x2);
}
}
对于intern分析步骤:先看一下当前字符串在StringTable
中是否存在,看jdk
版本决定是否要拷贝一份到StringTable
中。
使用总结:
JDK1.6
中,将这个字符串对象尝试放入串池。
● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
JDK1.7
起,将这个字符串对象尝试放入串池。
● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
6. StringTable的垃圾回收机制
掩饰垃圾回收:
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
首先来看一段什么也没有做的代码:
public static void main(String[] args) {
int i = 0;
try{
}catch (Throwable e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
0
Heap
PSYoungGen total 2560K, used 1484K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 2048K, 72% used [0x00000007bfd00000,0x00000007bfe73248,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 7168K, used 0K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)
Metaspace used 3157K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 347K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 12378 = 297072 bytes, avg 24.000
Number of literals : 12378 = 476656 bytes, avg 38.508
Total footprint : = 933816 bytes
Average bucket size : 0.619
Variance of bucket size : 0.621
Std. dev. of bucket size: 0.788
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 900 = 21600 bytes, avg 24.000
Number of literals : 900 = 60736 bytes, avg 67.484
Total footprint : = 562440 bytes
Average bucket size : 0.015
Variance of bucket size : 0.015
Std. dev. of bucket size: 0.122
Maximum bucket size : 2
对于
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 900 = 21600 bytes, avg 24.000
Number of literals : 900 = 60736 bytes, avg 67.484
Total footprint : = 562440 bytes
Number of buckets
代表的是stringtable
中数组的长度,而number of entries
代表的是键值对(也就是串池中string
的个数)的个数。
修改原来的代码,产生大量的没有引用的字符对象放到stringtable中。
public static void main(String[] args) {
int i = 0;
try{
for(int j = 0 ; j < 100000; j ++){
String.valueOf(j).intern();
i++;
}
}catch (Throwable e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
直接产生了GC信息
[GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 2048K->536K(9728K), 0.0023053 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 2584K->520K(9728K), 0.0012180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2544K->512K(2560K)] 2568K->552K(9728K), 0.0011401 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
100000
7. stringtable的性能调优
- 调整桶的个数
由于StringTable的底层是HashTable
,所以是有桶的概念在的,也就是数组的长度 。可以通过参数来设置:-XX:StringTableSize=1009
- 考虑将字符串对象是否入池
如果在系统中需要有大量的字符串可以使用intern
的方法将字符串入池,而不是直接使用默认放入串池的方法。
最难不过坚持