大纲
思考
-
如何查看字符串常量池(StringTable)?
使用 jclasslib 插件打开字节码,选择
常量池 -> 显示所选 -> CONSTANT_String_info
,左侧过滤后的内容即为字符串常量池 -
字符串常量池、方法区、永久代和元空间的关系?
方法区是逻辑概念,永久代和元空间是方法区的实现。字符串常量池总是在堆上:
- JDK7之前,方法区的实现是永久代,永久代是堆的一部分,而字符串常量池是永久代的一部分。
- JDK7时,方法区的实现是永久代,永久代是堆的一部分,字符串常量池也是堆的一部分,字符串常量池和永久代并列。
- JDK8及之后,方法区的实现是元空间,元空间是操作系统直接内存,字符串常量池还是堆的一部分。
-
Java中什么是常量?
static final
修饰的量就是常量吗?- 从字节码层面上看,字段有
ConstantValue
修饰的就是常量(包含对常量池的索引),如下图所示
- 从 Java 源代码层面上看
- 被
static final
修饰的int、double
等基本数据类型(不包含Integer
等包装类) - 被
static final
修饰的String
- 字面量,例如
1
或者"abcd"
- 被
注意事项:
static final String s = "abc"
中的 s 是常量,但static final String s = new String("abc")
中的 s 并不是常量,同样static final Integer num = 1
中的 num 也不是常量。 - 从字节码层面上看,字段有
-
字符串拼接问题
- 只有常量的情况,会存在编译优化,只会将最后生成的字符串添加到字符串常量池,例如
String s = "ab" + "cd"
字符串常量池中只会出现"abcd"
- 出现变量的情况,会创建
StringBuilder
对象,通过append()
来追加字符内容,最终调用toString()
来创建字符串对象。
- 只有常量的情况,会存在编译优化,只会将最后生成的字符串添加到字符串常量池,例如
测试 intern()
的作用
发现一个 bug,下面的代码如何批量测试时,internTest03()
方法的结果是 false、true。单独测试该方法时,返回结果是 true、true。
public class InternTest {@Testpublic void internTest01() {String s = new StringBuilder().append('a').append('b').append('c').append('d').toString();String intern = s.intern();System.out.println(intern == s);//true}@Testpublic void internTest02() {String s0 = "abcd";String s = new StringBuilder().append('a').append('b').append('c').append('d').toString();String intern = s.intern();System.out.println(intern != s);//trueSystem.out.println(intern == s0);//true}@Testpublic void internTest03() {String s = new StringBuilder().append('a').append('b').append('c').append('d').toString();String intern = s.intern();String s0 = "abcd";System.out.println(intern == s);//trueSystem.out.println(intern == s0);//true}}
测试创建多少个对象的代码
public class HowManyObjectTest {@Testpublic void howManyObjectTest01() {String s = "abcd";}@Testpublic void howManyObjectTest02() {String s = "ab" + "cd";}@Testpublic void howManyObjectTest03() {String s = new String("abcd");}@Testpublic void howManyObjectTest04() {String s = new String("ab" + "cd");// 验证字符串常量池中是否存在某个字符串的方法:// 还可以通过jclasslib直接查看字符串常量池中是否出现"ab"// String ab = new String("a") + new String("b");// String intern = ab.intern();// System.out.println(ab == intern); //true,证明"ab"是通过ab.intern()放入字符串常量池的,之前并不存在。}@Testpublic void howManyObjectTest05() {String s = new String("abcd") + "abcd";}static String s1 = "aaa";static String s2 = "bbb";@Testpublic void howManyObjectTest06() {String s = s1 + s2;}static final String s3 = "aaaa";static final String s4 = "bbbb";@Testpublic void howManyObjectTest07() {String s = s3 + s4;}static final String s5 = new String("aaaaa");static final String s6 = new String("bbbbb");@Testpublic void howManyObjectTest08() {String s = s5 + s6;}
}
问题一:new String("abcd")
会创建多少个对象?
0 new #3 <java/lang/String>3 dup4 ldc #2 <abcd>6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>9 astore_1
10 return
上面是 new String("abcd")
的字节码,下面对上面的内容进行解释:
-
new #3
:表示创建一个java.lang.String
类型的对象(分配对象空间),并将地址压入操作数栈。
(其中#3
表示字符串常量池中的索引,此时指向java.lang.String
) -
dup
:对操作数栈的栈顶元素进行复制,并压入操作数栈 -
ldc #2
:从常量池中加载字符串 “abcd” 对象,并压入操作数栈 -
invokespecial #4
:调用 String 类初始化方法init<>
,由于调用含参构造,因此消耗操作数栈中的两个参数
(从更通用的角度来看,这个init<>
的逻辑包含 (a)显示赋值、(b)代码块赋值、(c)构造器方法调用) -
astore_1
:将初始化完成的 String 对象的引用赋值给局部变量 s
问题二:new String("abcd") + "abcd"
会创建多少个对象?
0 new #5 <java/lang/StringBuilder>3 dup4 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>7 new #3 <java/lang/String>
10 dup
11 ldc #2 <abcd>
13 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 ldc #2 <abcd>
21 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore_1
28 return
-
new $5
、dup
、invokespecial #6
:创建一个 StringBuilder 对象,并初始化 -
new #3
、dup
、ldc #2
、invokespecial #4
:new String("abcd")
的代码逻辑 -
invokevirtual #7
:调用append()
方法,返回值类型还是 StringBuilder,消耗操作数栈中的两个元素,并返回一个元素注意:如果此时发生GC,那么 0x100 对应的 String 对象会被标记为垃圾。因为从 GCRoot 出发不可达。疑问:字符串常量池是否会进行垃圾回收?其中的对象在什么情况下会进行垃圾回收?
-
ldc #2
:直接压入字符串常量池中地址
-
invokevirtual #7
:再次调用append()
方法 -
invokespecial #8
:调用toString()
方法,创建 String 对象 -
astore_1
:赋值给局部变量 s
其它问题
- 验证
String s = "ab" + "cd"
执行完成后,字符串常量池中并不存在"ab"
和"cd"
- 使用 StringBuilder 对象调用
append()
逐个追加字符,最终得到的字符串不会放入到字符串常量池中 - 使用
char[]
来创建 String 对象,得到的字符串不会放入到字符串常量池中,StringBuilder 本质上维护了一个动态扩容的 char[] static final Integer num = 1
和static final String s = new String("abcd")
中的 num 和 s 均不是常量(可以通过clinit<>
的代码逻辑进行查看)