参考文章:JVM Class详解之二 Method字节码指令
首先要注意:
- 常量池≠字符串常量池(常量池有:字符串常量池、运行时常量池、静态常量池)
- 字符串常量池在堆中(JDK1.7觉得字符串经常创建和销毁,放在方法区不合适就改到了堆中,这样GC回收概率大些)
- 这里字节码文件删除了部分无关内容
- ldc指令将int、float或String型常量值从常量池中推送至栈顶
- 我发现个规律: 貌似在Constant pool中的Utf8都是字符串常量池的❓
字节码文件怎么看
主要看打⭐的地方,这里只是大致说明
Constant pool: /*⭐常量池,不是字符串常量池⭐*/
#1 = Methodref #11.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // java/lang/StringBuilder
#3 .... /*⭐这些记录了常量池的信息⭐*/
{
public StringTest(); /*⭐执行的类⭐*/
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable: /*⭐记录了编译出来的字节码指令和源码的对应关系⭐*/
line 10: 0
LocalVariableTable: /*⭐每个方法都有LocalVariableTable,是本地变量表。⭐*/
Start Length Slot Name Signature
0 5 0 this LStringTest;
public static void main(java.lang.String[]); /*⭐执行的方法⭐*/
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4:...... /*⭐执行的代码⭐*/
LineNumberTable: /*⭐记录了编译出来的字节码指令和源码的对应关系⭐*/
line 15: 0
line 17: 35
LocalVariableTable: /*⭐每个方法都有LocalVariableTable,是本地变量表。⭐*/
Start Length Slot Name Signature
0 36 0 args [Ljava/lang/String;
35 1 1 s4 Ljava/lang/String;
}
面试题一:String s1 = “a” 创建了多少个对象?
创建了0个或1个对象,来看看该字节码文件,在#2可以看到创建了对象a(字符串常量池中),如果字符串a在执行到该句之前就存在字符串常量池则创建0个对象,否则在堆中的字符串常量池创建1个对象
Constant pool:
.....
{
public static void main(java.lang.String[]);
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // ⭐String a
2: astore_1
3: return
LineNumberTable:
line 12: 0
line 17: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 s1 Ljava/lang/String;
}
面试题二:String s2 = new String(“b”) 创建了多少个对象?
创建1个或2个,该句的执行逻辑是:JVM先在堆中创建String对象,然后查看字符串常量池是否存在b,存在就直接返回字符串引用,不存在就创建然后再返回字符串引用。来看看该字节码文件,可以看到#2先创建了String对象(堆中),然后#3创建了b对象(字符串常量池中)
Constant pool:
.....
{
public static void main(java.lang.String[]);
Code:
stack=3, locals=2, args_size=1
0: new #2 // ⭐class java/lang/String
3: dup
4: ldc #3 // ⭐String b
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
LineNumberTable:
line 13: 0
line 17: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
10 1 1 s2 Ljava/lang/String;
}
面试题三:String s2 = “a” + “b” 创建了多少个对象?
创建0个或1个对象,因为这里会经过编译器优化,等价于String s2 = “ab”,所以和面试题一是一样的,就不展示字节码文件了
面试题四:String s1 = “a”; String s2 = new String(“b”);String s3 = s1 + s2;第三句创建了多少个对象?
2个对象,说明如下:
执行第一句时,创建了0个或1个对象;第二句创建1个或2个(同面试题二);执行第三句时,会先创建StringBuilder对象(见字节码#6),等价于new StringBuilder().append("a").append("b").toString()
,由于toString()
也是会创建一个String对象,所以第三句是创建了两个对象(ab没有放在字符串常量池)
Constant pool:
......
{
public static void main(java.lang.String[]);
Code:
stack=3, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #4 // String b
9: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: new #6 // ⭐class java/lang/StringBuilder
16: dup
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
20: aload_1
21: invokevirtual #8
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_2
25: invokevirtual #8
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #9 // ⭐Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_3
32: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 13
line 17: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 s1 Ljava/lang/String;
13 20 2 s2 Ljava/lang/String;
32 1 3 s3 Ljava/lang/String;
}
面试题五:String s4 = new String(“a”) + new String(“b”)创建了多少个对象?(假设字符串常量池在此之前为空)
6个;首先会创建一个StringBuilder对象用来等会拼接字符串,然后new String(“a”)和new String(“b”)分别各创建了2个对象(共4个),StringBuilder的toString()
方法会new String(value,0,count)
再创建一个对象,注意在字符串常量池中中没有ab(因为编译期间不能确定)。(⭐刚好对应创建的6个对象)
Constant pool:
......
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, 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/Stri
ngBuilder;
19: new #4 // ⭐class java/lang/String
22: dup
23: ldc #8 // ⭐String b
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
31: invokevirtual #9 //⭐ Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
LineNumberTable:
line 15: 0
line 17: 35
LocalVariableTable:
Start Length Slot Name Signature
0 36 0 args [Ljava/lang/String;
35 1 1 s4 Ljava/lang/String;
}
---
总结
可以看出,如果是编译器可以确定的,字符串就会放入到字符串常量池,否则动态创建的字符串只会在堆中的对象,不会放入字符串常量池(字符串常量池也在堆中),除非调用String.intern()
方法,其作用是将指定的字符串对象的引用保存在字符串常量池中
- 2022.10.03 第一次修正
– 修正了题四的错误 - 2023.03.10 第二次修正
– 修正了题四、五错误,精简了文章篇幅,删掉了大量无用的字节码篇幅