我一直对.class文件编译之后的内容比较感兴趣,我们现在一起来揭秘吧!
我们编写如下的测试代码,我们想要去看看编译出来的.class文件中究竟包含了什么?
package com.wanna.jdkset.TestString;
public class TestString {
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer();
buffer.append("wanna");
}
}
这里需要借用到IDEA工具中的jclasslib插件,或者是javap命令去进行反编译。
我们用到的命令是javap -v com.wanna.jdkset.TestString
,去进行编译成为字节码,最终得到如下的信息
public class com.wanna.jdkset.TestString
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = Class #24 // java/lang/StringBuffer
#3 = Methodref #2.#23 // java/lang/StringBuffer."<init>":()V
#4 = String #25 // wanna
#5 = Methodref #2.#26 // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#6 = Class #27 // com/wanna/jdkset/TestString
#7 = Class #28 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/wanna/jdkset/TestString;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 buffer
#20 = Utf8 Ljava/lang/StringBuffer;
#21 = Utf8 SourceFile
#22 = Utf8 TestString.java
#23 = NameAndType #8:#9 // "<init>":()V
#24 = Utf8 java/lang/StringBuffer
#25 = Utf8 wanna
#26 = NameAndType #29:#30 // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#27 = Utf8 com/wanna/jdkset/TestString
#28 = Utf8 java/lang/Object
#29 = Utf8 append
#30 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuffer;
{
public com.wanna.jdkset.TestString();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wanna/jdkset/TestString;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/StringBuffer
3: dup
4: invokespecial #3 // Method java/lang/StringBuffer."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String wanna
11: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
14: pop
15: return
LineNumberTable:
line 18: 0
line 19: 8
line 20: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 buffer Ljava/lang/StringBuffer;
}
我们来看开头的一部分
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_SUPER
minjor和major这两个主要是和字节码版本相关的。flags主要就是类的修饰符,ACC_SUPER
代表这个类有父类,ACC_PUBLIC
代表这个类是public
修饰的。
我们先截取常量池的一部分,其它暂时不看
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = Class #24 // java/lang/StringBuffer
#3 = Methodref #2.#23 // java/lang/StringBuffer."<init>":()V
#4 = String #25 // wanna
#5 = Methodref #2.#26 // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#6 = Class #27 // com/wanna/jdkset/TestString
#7 = Class #28 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/wanna/jdkset/TestString;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 buffer
#20 = Utf8 Ljava/lang/StringBuffer;
#21 = Utf8 SourceFile
#22 = Utf8 TestString.java
#23 = NameAndType #8:#9 // "<init>":()V
#24 = Utf8 java/lang/StringBuffer
#25 = Utf8 wanna
#26 = NameAndType #29:#30 // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#27 = Utf8 com/wanna/jdkset/TestString
#28 = Utf8 java/lang/Object
#29 = Utf8 append
#30 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuffer;
我们可以看到,常量池中的Class的部分主要包括下面这几个,它主要存储的就是我们的Class的相关信息。
#2 = Class #24 // java/lang/StringBuffer
#6 = Class #27 // com/wanna/jdkset/TestString
#7 = Class #28 // java/lang/Object
我们可以看到,这三个Class分别指向了#24
、#27
以及#28
,我们再从常量池中找到这三个内容,我们发现它们最终都是Utf8的字符串,而这些字符串的内容就是我们会用到的相关类的全限定名。(其实就是注释中的内容,这应该是javap工具帮我们做的)
我们再来查看Methodref部分,主要包括如下几个
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#3 = Methodref #2.#23 // java/lang/StringBuffer."<init>":()V
#5 = Methodref #2.#26 // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
第一个Methodref就是#7.#23
,查找常量池可以发现它的内容就是java/lang/Object."<init>":()V
,我们首先观察这个内容?
- 1.
java/lang/Object
很显然就是类的全限定名; - 2.
"<init>":()V
呢?首先我们查看常量池中的#23
元素,发现它的类型是NameAndType
。- 很显然
<init>
其实就是构造器方法的名称 -
()V
代表的是什么意思呢?这个玩意叫做方法的Descriptor(描述符),它的作用就是用来描述方法的参数和返回值。()V
中的()
就代表方法是没有参数的,V
就代表了它的返回值是void。 - 根据以上两点,我们可以发现,JVM中把方法名、参数类型以及返回值类型封装到一个NameAndType对象中。
- 很显然
我们可以发现构造器方法,其实本质上也只是一个方法罢了,只不过方法名叫<init>
。
第二个Methodref是#2.#23
,这个和上一个Methodref很类似。
第三个Methodref是#2.#26
,查看常量池可以发现它的内容是java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
,我们也是进行分解:
- 1.
java/lang/StringBuffer.append
代表的就是我们调用的StringBuffer
的方法的方法名,方法名是使用的类全限定名.方法名
的方式去进行标识的。 - 2.
(Ljava/lang/String;)Ljava/lang/StringBuffer;
代表的就是append方法的描述符,使用到的方法的参数是Ljava/lang/String;
,而该方法的返回值类型是Ljava/lang/StringBuffer;
。(其中L代表的是它是一个引用类型)
下面要看的就是方法对应的字节码了
public com.wanna.jdkset.TestString();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wanna/jdkset/TestString;
我们可以看到方法名是com.wanna.jdkset.TestString
,方法的描述符是()V
,访问标识符是ACC_PUBLIC
。
stack=1, locals=1, args_size=1
说明栈的最大为1,局部变量表的大小是1,参数数量是1。
下面就是方法的主要执行部分的字节码了
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
先通过aload_0
,把局部变量表中的0号槽位的内容(this)压入栈中,然后使用invokespecial #1
去执行Object类的空参构造器方法,然后使用return
将构造器的栈帧销毁,回到调用方法的栈帧。
然后就是行号表(建立的是字节码的行号和java代码的行号之间的对应关系)
LineNumberTable:
line 9: 0
然后是局部变量表
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wanna/jdkset/TestString;
我们可以看到,局部变量表中居然放了个this对象?其实所有非静态方法的局部变量表中的0号槽位(slot)的元素都是this,相当于我们所有非静态方法的第一个参数都是this(你也可以在第一个参数位置写个this,但是你不写编译器帮你做了)。
比如下面两种方式都是完全等价的,如果你写了this,编译器就不会再帮你做了,肯定会往局部变量表的槽位0中放入this,如果你没写,那么编译器自然会帮你完成。
// method01:
public void test(Test this, String str) {
}
// method02:
public void test(String str) {
}
接着来看main方法的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/StringBuffer
3: dup
4: invokespecial #3 // Method java/lang/StringBuffer."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String wanna
11: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
14: pop
15: return
LineNumberTable:
line 18: 0
line 19: 8
line 20: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 buffer Ljava/lang/StringBuffer;
首先我们可以直观的看到,局部变量表的槽位0位置放的不是this,而是args,槽位1中则是放的就是buffer
的引用,其它就没什么说的。
网友评论