上面研究类的文件结构时,观察到方法体内有一些字节,他们就是对应着将来要由Java虚拟机执行的方法内的代码。在构造方法里(Hello World()
)里,有五个字节的代码。2a b7 00 01 b1
,这些2a
之类的,实际上对应着字节码的指令(五个字节中的2a b7 b1
),Java虚拟机内部有解释器,这个解释器能够识别这些平台无关的字节码指令,把它们最终解释为机器码,然后执行。2a
对应的指令是aload_0 = 42
(0x2a),前面的aload_0
是助记符,便于我们人类理解的,真正Java虚拟机解释的还是后面的2a
字节。b7
是 invokespecial
= 183(0xb7),b1
是 return
= 177(0xb1)。
aload_0
的作用就是把局部变量表中一个变量给他加载到操作数栈上,因为Java虚拟机的解释器执行的时候他大部分都是要到操作数栈上去读取那个数据,aload_0
就是把局部变量表中0号槽位
的变量加载到这个操作数栈上,这里的0号变量就是this。那么用this准备执行什么操作呢,就是接下来的b7
,即invokespecial
,就是准备去进行一个方法的调用(就比如是 this.
),调用哪个方法呢,00 01
引用常量池中#1
项,即【Method java/lang/Object."<init>":()V
】,即是Object中的构造方法,()V
说明无参、返回值类型是void
,所以从2a
到01
就是通过this调用了无参的构造方法,当然他调用的不是本类的,如果自己掉自己就是递归了,他调的是父类的,b1
表示返回,执行完了方法后return
。
在main方法里有9个字节代码。b2 00 02 12 03 b6 00 04 b1
。b2
对应的是getstatic
,用来加载静态变量,把他加载到操作数栈,哪个静态变量呢,00 02
引用常量池中#2
项,即【Field java/lang/System.out:Ijava/io/PrintStream;
】,即是System类中的out这个静态变量,Ijava/io/PrintStream是out变量的类型,即b2 00 02
是把这个静态变量加载到操作数栈,加载进来之后,在方法调用时还需要参数,12
是代表lbc
,即加载常量池中的参数,哪个参数呢,03
引用常量池中#3
项,即【String hello world
】,即12 03
代表准备好了一个“hello world”这样一个常量,那么他两(System.out变量和hello world
)都被放到操作数栈上,接着b6
是invokevirtual
,是代表执行一个普通的成员方法,也就是“.
”开始要执行方法,那执行哪个方法呢,00 04
引用常量池中#4项,即要执行【Method java/io/PrintStream.println:(Ljava/lang/String;)V
】,即是PrintStream
这个类中println
这个方法,方法的参数是接受一个字符串,没有返回值,b1
表示返回。(值得注意的是这里和Java源代码的编写习惯是不一样的,比如Java源代码是先把对象System.out准备好,再去“.”方法println,最后再加参数“hello world”,但是在Java虚拟机的解释器执行时,他的操作数栈先得把对象和参数准备好,然后再去调方法。
)
javap 工具
自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件。具体用法很简单:javap -v HelloWorld.class
。其中-v
参数是指要输出类文件的详细信息。若没有-v
参数,他只会显示出最基本的结果,而不会显示出那些常量池之类的信息就不会显示出。执行之后输入如下:
Classfile /root/HelloWorld.class
Last modified Jul 7, 2022; size 597 bytes【最后修改时间,大小是597字节(文件本身的信息)】
MD5 checksum 361dca1c3frae348rra9cd5060ac6dbc【MD5的校验签名(文件本身的信息)】
Compiled from "HelloWorld.java"【Java源文件对应的是HelloWorld.java】
public class com.waca.HelloWorld【类的全路径】
minor version: 0
major version: 52【JDK8】
flags: ACC_PUBLIC, ACC_SUPER【类的访问修饰符(一个public的一个类)】
Constant pool:【常量池】
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V【方法引用。查询结果直接以注释的方式写在后面,以下同。】
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;【表示要引用一个成员变量,以下省略】
#3 = String #24 // hello world
#4 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #27 // com/woca/HelloWorld
#6 = Class #28 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10= Utf8 LineNumberTable
#11= Utf8 LocalVariableTable
#12= Utf8 this
#13= Utf8 Lcom/waca/HelloWorld;
#14= Utf8 main
#15= Utf8 ([Ljava/lang/String;)V
#16= Utf8 args
#17= Utf8 [Ljava/lang/String;
#18= Utf8 MethodParameters
#19= Utf8 SourceFile
#20= Utf8 HelloWorld.java
#21= NameAndType #7.#8 // "<init>":()V
#22= Class #29 // java/lang/System
#23= NameAndType #30:#31 // out:Ljava/io/PrintStream;
#24= Utf8 hello world
#25= Class #32 // java/io/PrintStream
#26= NameAndType #32:#34 // println:(Ljava/lang/String;)V
#27= Utf8 com/waca/HelloWorld
#28= Utf8 java/lang/Object
#29= Utf8 java/lang/System
#30= Utf8 out
#31= Utf8 Ljava/io/PrintStream;
#32= Utf8 java/io/PrintStream
#33= Utf8 println
#34= Utf8 (Ljava/lang/String;)V
【大括号开始是方法信息,有两段。第一段是构造方法,第二段是main方法。】
{
public com.waca.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1【最大操作栈深度,局部变量表长度,参数的长度】
0: aload_0【这个以及下面两个是上面分析的字节码。aload_0是把局部变量中的第0项加载到操作数栈,然后紧接着调用invokespecial,要调用的方法是常量池#1,是init方法。0 1 4是代表字节码的代码行号。】
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:【Code属性的子属性】
line 4: 0【4代表Java源代码中的行号,0代表字节码中的行号】
LocalVariableTable:【对应着本地变量表,0是起始范围,5是变量的作用范围,即上面的0 ~ 4行。变量名是this,类型是HelloWorld。】
Start Length Slot Name Signature
0 5 0 this Lcom/waca/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PirntStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}
可以看得出,这种信息比起那种阅读二进制字节码肯定要方便很多了。
图解方法内的代码执行流程
// 演示 字节码指令 和 操作数栈、常量池的关系
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;// 这里故意让他突破一个界限,因为这个分界线,待会就会看到这个数字值到底存储在什么位置
int c = a + b