本文介绍JVM中String以及字符串常量池的相关知识。在运行时数据区已经了解了,字符串常量池和静态变量都是存储在堆中的。
1. 概述
String就是字符串,使用一对双引号引起来表示。有两种实例化方式:
String s = "hello";
以字符串形式定义,存储在字符串常量池中。String s = new String("hello");
以对象形式定义,存储在堆中。
String声明是final,不可被继承,实现了Serializable接口、Comparable接口。
在JDK1.8的时候,String底层使用的是char数组;在JDK1.9及之后,String底层使用的是byte数组。因为char用的是两个byte,但是在实际生产环境中,堆空间用的数据主要是String,而且存储的都是一些基本字符,完全可以用其ISO-8859-1/Latin-1编码值来表示,这些编码值可以用一个byte来表示,这样就节省了空间。
但是一个byte只能表示基本字符,对于中文仍然不行,需要两个byte来表示。因此,需要一个encoding-flag来表示,如果是UTF-8,则用的是两个字节。
同样,StringBuffer和StringBuilder也进行了相应修改。
字面量的相关规范可参考官网Chapter 3. Lexical Structure (oracle.com)。
1.1 字符串常量池
注意,字符串常量池中是不会存储相同内容的字符串的。
字符串常量池是一个固定大小的HashTable。如果放进String Pool的String非常多,就会造成Hash冲突,从而导致列表会很长,而链表长了之后直接会造成的影响就是当调用String.intern()时性能会大幅下降。
这里的意思就是说,首先是Hash运算,如果字符串特别多,或者数组长度太小,显然会冲突,此时就放在对应的链表中,那么此时检索链表,链表的检索效率是比较低的,因此,如果元素过多,效率会大幅下降。
可使用-XX:StringTableSize
设置HashTable大小,即数组的长度。
1.2 面试题
new String(“ab”)会创建几个对象?
2个对象,new String()会在堆中创建一个对象,”ab”会在常量池中创建对象(字节码指令ldc)。
new String(“a”) + new String(“b”)会创建几个对象?
6个对象,StringBuilder对象,new String(“a”),”a”,new String(“b”),”b”,
StringBuilder.toString()创建的对象:new String(“ab”)
注意,字符串常量池中,不存在”ab”。
2. String的内存分配
在Java语言中有8中基本数据类型和一种比较特殊的类型String,这些类型为了使他们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念(基本数据类型的包装类型)。
String类型对象有两种方式,一种是new对象,一种是字符串。采用字符串可以直接在常量池中放一个该字符串,而采用new则需要在堆中创建该对象,如果想要在常量池中放一个字符串,可调用String对象.intern()
方法将该值放入到常量池中。
3. 字符串拼接操作
这里的拼接操作指的是加号运算。字符串拼接有如下规则:
- 常量与常量的拼接结果在常量池,原理是编译器优化;
- 常量池中不会存在相同内容的常量;
- 只要其中有一个是变量(final修饰的除外,是常量),结果就在堆中。变量拼接的原理是StringBuilder;
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
1 | public void m1() { |
1 | public void m2() { |
1 | public String m3() { |
3.1 StringBuilder替换字符串拼接
其实上面变量字符串拼接,底层是采用StringBuilder,然后append,最后toString创建字符串对象,并返回该字符串对象。
回想在写程序的时候,涉及到循环字符串拼接,IDEA总是提示采用StringBuilder。这里就是因为加号字符串拼接,每次都创建一个StringBuilder对象。案例如下所示,显然下面的效率要高于上面的。因此,尽量少采用加号字符串拼接,除了创建对象比较多,而且GC的话,需要清理的垃圾也比较多。
1 | String src = ""; |
4. intern()的使用
字符串对象.intern()
方法,判断字符串常量池中是否存在这个字符串值,如果存在,则返回常量池中该字符串的地址;如果不存在,则在常量池中加载一份该字符串,并返回常量池中该字符串的地址。该方法是本地方法。
注意,无论前面是什么,只要最终是intern()方法,返回的都是该字符串在常量池中的地址,如下所示:
1 | String s = "asdf"; |
补充,上面调用intern()方法,在JDK7及之后,其实不会再在常量池中创建字符串了,而是在常量池中开辟一块空间,保存的就是堆对象的地址【引用】,返回的就是堆对象的那个地址。
但是
new String()
一定是在堆中创建一个对象,并且在常量池中也创建一个对象。
5. G1中的String去重操作
注意,字符串常量池中本身就不存在重复的字符串,所以这里的去重指的是堆内存中的对象去重。
对许多Java应用做的测试得出以下结果:
- 堆存活的数据集合里面String对象占了25%。
- 堆存活的数据集合里面String对象有13.5%。
- String对象的平均长度是45。
G1垃圾回收器对String对象进行了去重。默认是不开启的。
6. 备注
参考B站《尚硅谷》。