接上节。
示例代码:
package com.lkl.jvmDemo;
public class HelloByteCode {
public static void main(String[] args) {
HelloByteCode obj = new HelloByteCode();
}
}
查看字节码清单javap -c -verbose HelloByteCode
Classfile /XXX/com/lkl/jvmDemo/HelloByteCode.class
Last modified 2023-10-29; size 304 bytes
MD5 checksum 565e4ca34e83f69df37c1f35c971375f
Compiled from "HelloByteCode.java"
public class com.lkl.jvmDemo.HelloByteCode
minor version: 0
major version: 65
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // com/lkl/jvmDemo/HelloByteCode
#8 = Utf8 com/lkl/jvmDemo/HelloByteCode
#9 = Methodref #7.#3 // com/lkl/jvmDemo/HelloByteCode."<init>":()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 HelloByteCode.java
{
public com.lkl.jvmDemo.HelloByteCode();
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 3: 0
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 #7 // class com/lkl/jvmDemo/HelloByteCode
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
方法体中的字节码解读
0: new #7 // class com/lkl/jvmDemo/HelloByteCode
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: return
main方法体中那些字节码指令前面的数字是什么意思,说是序号吧但又不太像,因为他们之间的间隔不相等。
间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。
new
就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。因此,下一条指令 dup
的索引从 3
开始。
这个方法体变成可视化数组,那么看起来应该是这样的:
每个操作码/指令都有对应的十六进制(HEX)表示形式, 如果换成十六进制来表示,则方法体可表示为HEX字符串。例如上面的方法体百世成十六进制如下所示:
上述02改为07(对应字节码中#7),支持十六进制的编辑器中打开 class 文件,可以在其中找到对应的字符串:
对象初始化指令:new 指令, init 以及 clinit 简介
new
是 Java 编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new
。 创建类的实例时, 编译器会生成类似下面这样的操作码:
0: new #7 // class com/lkl/jvmDemo/HelloByteCode
3: dup
4: invokespecial #9 // Method "<init>":()V
同时看到 new, dup
和 invokespecial
指令在一起时,那么一定是在创建类的实例对象!
为什么是三条指令而不是一条呢?这是因为:
new
指令只是创建对象,但没有调用构造函数invokespecial
指令用来调用某些特殊方法的,当然这里调用的是构造函数。dup
指令用于复制栈顶的值。
由于构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。
这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段。因此,接下来的那条指令一般是以下几种:
astore {N}
orastore_{N}
– 赋值给局部变量,其中{N}
是局部变量表中的位置。putfield
– 将值赋给实例字段putstatic
– 将值赋给静态字段
调用构造函数的时候,其实还会执行另一个类似的方法 <init>
,甚至在执行构造函数之前就执行了。
还有一个可能执行的方法是该类的静态初始化方法 <clinit>
, 但 <clinit>
并不能被直接调用,而是由这些指令触发的: new
, getstatic
, putstatic
or invokestatic
。
也就是说,如果创建某个类的新实例, 访问静态字段或者调用静态方法,就会触发该类的静态初始化方法【如果尚未初始化】。