Java的JVM字节码指令集详解【一万字】
本文详细介绍了如何使用javap查看java方法中的字节码、以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出。在文末还附有JVM字节码指令集表。
本文不适合没有JVM基础的初学者,看文章之前希望你有如下基本技能:了解JVM的一些基本概念,比如什么是符号引用?什么是字面量?了解class文件的基本结构,了解基于栈的JVM方法执行的栈基本结构!关于class文件的结构可以看:Java的 Class字节码文件结构和内容全面解析【两万字】。
看完本文,你可能获得如下知识:JVM字节码的入门、如何查看和分析字节码的执行、加深对JVM中的栈的理解,基于字节码层面,或许还能为你解开一些原始的疑惑,比如:为什么finally块中的语句总会被执行?
1 字节码概述
字节码是一套设计用来在Java虚拟机中执行的高度优化的指令集。Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令,或者说是Java代码的在JVM中的最小执行单元。JVM只关心字节码,而不关心源文件是属于在哪个平台用哪种语言编写的,字节码是实现JVM平台无关性和语言无关性的基石。
JVM字节码指令由一个字节长度的、代表着某种特定操作含义的数字称为操作码,Opcode),以及跟随其后的零到多个代表此操作所需参数(称为操作数,Operands)构成。
所谓一个字节的长度,也就是8位二进制数字,也就是两位十六进制数字。
每一个字节码还拥有它对应的助记符形式。顾名思义,助记符就是帮助我们查看、理解字节码的含义的符号,一般我们看字节码操作都通过是看助记符来分辨的。但是实际的执行运行并不存在助记符这些东西,都是根据字节码的值来执行。
由于操作码的长度为一个字节,因此操作码(指令)最多不超过256条。
对于操作数长度超过了一个字节的情况(取决于操作码):由于Class文件格式放弃了编译后代码的操作数长度对齐,所以,当JVM处理超过一个字节长度的数据时,需要在运行时从字节中重建出具体数据的结构,如16位二进制数据需要(byte1 << 8)|byte2操作(Big-Endian 顺序存储——即高位在前的字节序)。
2 基于javap的字节码解析
2.1 javap介绍
如果直接打开class文件,只能查看数字形式的字节码。
我们可以使用javap工具,反编译class文件,将数字类型的字节码转换成我们能看懂的格式,进而快捷的查看一个java类的常量池、方法表、code属性(方法字节码)、局部变量表、异常表等等信息。在这里,我认为javap反编译之后输出的数据,并不是传统意义上的汇编指令,只是The Java Virtual Machine Instruction Set(Java 虚拟机指令集)的一种方便人们理解的表现形式。
javap的格式为: javap < options > < classes >
其中options选项如下:
-version | 版本信息,其实是当前javap所在jdk的版本信息,不是cass在哪个jdk下生成的。 |
-l | 输出行号和本地变量表 |
-c | 对代码进行反汇编,主要是针对方法的代码 |
-v -verbose | 不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等详细信息。 |
-pubic | 仅显示公共类和成员 |
-protected | 显示受保护的/公共类和成员 |
-package | 显示程序包/受保护的/公共类 和成员 (默认) |
-p -private | 显示所有类和成员 |
-s | 输出内部类型签名 |
-sysinfo | 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) |
-constants | 显示静态最终常量 |
-classpath | 指定查找用户类文件的位置 |
-bootclasspath | 覆盖引导类文件的位置 |
一般常用的是-v -l -c三个选项,-v展示的最全面。官方javap介绍地址为:javap
2.2 javap案例
2.2.1 源码类
源码类使用在介绍class文件结构时候使用的源码类:Java的 Class字节码文件结构和内容全面解析【两万字】。方便连续学习。
这里再贴出来一下:
public class ClassFile {
public static final String J = "2222222";
private int k;
public int getK() {
return k;
}
public void setK(int k) throws Exception {
try {
this.k = k;
} catch (IllegalStateException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) {
}
}
2.2.2 javap输出解析
对源码的class文件,使用javap -v ClassFile.class
指令,得到如下信息(灰色的文字是后来自己加的注释)。
对于初学者,应该一行行的仔细查看其含义,对于重要的部分单独拿出来讲解,比如方法字节码部分。
Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class //生成该class文件的源文件名称和路径
Last modified 2020-4-8; size 960 bytes //上一次修改时间和大小
MD5 checksum fcd8ef238722b6dab50c0495a9673a1f //MD5信息
Compiled from "ClassFile.java" //编译自ClassFile.java类
public class com.ikang.JVM.classfile.ClassFile
minor version: 0 //小版本号
major version: 52 //大版本号
flags: ACC_PUBLIC, ACC_SUPER //类访问标记, ACC_PUBLIC表示public, ACC_SUPER表示允许使用invokespecial字节码指令的新语意
//class常量池开始,可以看出有46个常量
Constant pool:
//第1个常量是CONSTANT_Methodref_info类型,表示类中方法的符号引用,具有两个索引属性,分别指向第6个和第37个常量,其最终值为java/lang/Object."<init>":()V
//这明显是< init >方法 的符号引用,编译时自动生成的。
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
//第2个常量是CONSTANT_Fieldref_info类型,表示类中字段的符号引用,具有两个索引属性,分别指向第5个和第38个常量 其最终值为com/ikang/JVM/classfile/ClassFile.k:I
//这明显是k字段的符号引用
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
//第3个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第39个常量 其最终值为java/lang/IllegalStateException
//这明显是引入的IllegalStateException异常类的符号引用
#3 = Class #39 // java/lang/IllegalStateException
//第4个常量是CONSTANT_Methodref_info类型,表示类中方法的符号引用,具有两个索引属性,分别指向第3个和第40个常量,其最终值为java/lang/IllegalStateException.printStackTrace:()V
//这明显是e.printStackTrace()方法的符号引用
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
//第5个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第41个常量 其最终值为com/ikang/JVM/classfile/ClassFile
//这明显是本类(ClassFile类)的符号引用
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
//第6个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第41个常量 其最终值为java/lang/Object
//这明显是本类的父类Object的符号引用
#6 = Class #42 // java/lang/Object
//第7个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是J
//这明显储存的常量J的名称的字面值
#7 = Utf8 J
//第7个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/String;
//这明显表示一个对象类型的字段描述符的字面值,String类型
#8 = Utf8 Ljava/lang/String;
//第9个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是ConstantValue
//这明显表示常量字段的ConstantValue属性的名字的字符串字面值
#9 = Utf8 ConstantValue
//第10个常量是CONSTANT_String_info类型,表示字符串类型字面量,用来表示类型,具有一个指向具体字面值的索引43, 这里的具体值是2222222
//这明显是常量 J的字面量,因为是字符串类型
#10 = String #43 // 2222222
//第11个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k
//这明显储存的变量J的名称的字面值
#11 = Utf8 k
//第12个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 I
//这明显表示一个int类型的字段描述符的字面值
#12 = Utf8 I
//第13个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 <init>
//这明显表示<init>方法名称的字面值
#13 = Utf8 <init>
//第14个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ()V
//这明显表示一个返回值为void类型、没有参数的方法描述符的字面值,那么可以表示构造方法的方法描述符的字面值
#14 = Utf8 ()V
//第15个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 Code
//这明显表示方法中的Code属性的名称的字面值
#15 = Utf8 Code
//第16个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k
//这明显表示方法的Code属性中的LineNumberTable属性的名称的字面值
#16 = Utf8 LineNumberTable
//第17个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k
//这明显表示方法的Code属性中的LocalVariableTable属性的名称的字面值
#17 = Utf8 LocalVariableTable
//第18个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 this
//这明显表示方法的局部变量this的名称的字面值
#18 = Utf8 this
//第19个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 Lcom/ikang/JVM/classfile/ClassFile;
//这明显表示一个对象类型的字段描述符的字面值,ClassFile类型
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
//第20个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 getK
//这明显表示getK方法名称的字面值
#20 = Utf8 getK
//第21个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ()I
//这明显表示一个返回值为int类型、没有参数的方法描述符的字面值,那么可以表示getK方法的方法描述符的字面值
#21 = Utf8 ()I
//第22个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 setK
//这明显表示setK方法名称的字面值
#22 = Utf8 setK
//第23个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 (I)V
//这明显表示一个返回值为void类型、参数为int类型的方法描述符的字面值,那么可以表示setK方法的方法描述符的字面值
#23 = Utf8 (I)V
//第24个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 e
//这个e是什么呢?实际上是表示参数的名称的字面值
#24 = Utf8 e
//第25个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/IllegalStateException;
//这明显表示一个对象类型的字段描述符的字面值,IllegalStateException类型
#25 = Utf8 Ljava/lang/IllegalStateException;
//第26个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/IllegalStateException;
//这明显表示方法的Code属性中的StackMapTable属性的名称的字面值
#26 = Utf8 StackMapTable
//第27个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第39个常量 其最终值为java/lang/IllegalStateException
//这明显是IllegalStateException类的符号引用
#27 = Class #39 // java/lang/IllegalStateException
//第28个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第44个常量 其最终值为java/lang/Throwable
//这明显是Exception的父类Throwable类的符号引用
#28 = Class #44 // java/lang/Throwable
//第29个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Exceptions
//加了个s,肯定不是类名字面量了,实际上它是方法表中的Exceptions异常表属性的名字 字符串字面量
#29 = Utf8 Exceptions
//第30个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第45个常量 其最终值为java/lang/Exception
//这明显是Exception类的符号引用
#30 = Class #45 // java/lang/Exception
//第31个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是main
//这明显是main方法的方法名字 字符串字面量
#31 = Utf8 main
//第32个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ([Ljava/lang/String;)V
//这明显表示一个返回值为void类型、参数为String一维数组类型的方法描述符的字面值,那么可以表示main方法的方法描述符的字面值
#32 = Utf8 ([Ljava/lang/String;)V
//第33个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ([Ljava/lang/String;)V
//这明显表示main方法的参数名的字符串字面量
#33 = Utf8 args
//第34个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 [Ljava/lang/String;
//这明显表示一个对象类型的字段描述符的字面值,String[]类型
#34 = Utf8 [Ljava/lang/String;
//第35个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 SourceFile
//这明显表示SourceFile属性的名字的字面值
#35 = Utf8 SourceFile
//第36个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ClassFile.java
//这明显表示SourceFile属性的具体值的字面值
#36 = Utf8 ClassFile.java
//第37个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向13和14个字符串,这里的最终值是 "<init>":()V,我们回过头去看第13和14个字符串,它们储存的是具体的字面值。
//这个常量被我们的第1个常量持有,即表示< init >方法的部分符号引用
#37 = NameAndType #13:#14 // "<init>":()V
//第38个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向11和12个字符串,这里的最终值是 k:I,我们回过头去看第11和12个字符串,它们储存的是具体的字面值。
//这个常量被我们的第2个常量持有,即表示k字段的部分符号引用
#38 = NameAndType #11:#12 // k:I
//第39个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/IllegalStateException
//这个常量被我们的第3、27个常量持有,这明显表示储存IllegalStateException类的符号引用的具体字面值
#39 = Utf8 java/lang/IllegalStateException
//第40个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向46和14个字符串,这里的最终值是 k:I,我们去看第46和14个字符串,它们储存的是具体的字面值。
//这个常量被我们的第4个常量持有,即表示printStackTrace方法的部分符号引用
#40 = NameAndType #46:#14 // printStackTrace:()V
//第41个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 com/ikang/JVM/classfile/ClassFile
//这个常量被我们的第5个常量持有,这明显表示储存本类(ClassFile类)的符号引用的具体字面值
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
//第42个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 com/ikang/JVM/classfile/ClassFile
//这个常量被我们的第6个常量持有,这明显表示储存本Object类的符号引用的具体字面值
#42 = Utf8 java/lang/Object
//第43个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 2222222
//这个常量被我们的第10个常量持有,这明显表示储存常量J的具体值
#43 = Utf8 2222222
//第44个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/Throwable
//这个常量被我们的第28个常量持有,这明显表示储存Exception的父类Throwable类的符号引用具体值
#44 = Utf8 java/lang/Throwable
//第45个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/Throwable
//这个常量被我们的第30个常量持有,这明显表示储存Exception类的符号引用具体值
#45 = Utf8 java/lang/Exception
//第46个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 printStackTrace
//这个常量被我们的第40个常量持有,这明显表示printStackTrace方法名的具体值
#46 = Utf8 printStackTrace
//class常量池结束,后面是字段表和方法表
{
//常量字段J
public static final java.lang.String J;
//常量字段J的字段描述符 String类型
descriptor: Ljava/lang/String;
//常量字段J的访问标记 public static final
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
//常量字段J的额外ConstantValue属性,该属性中又包括常量字段的类型和具体值,这里可以看出来,对于常量字段在编译时就确定了值,对于常量字段,在类加载的准备阶段就已经通过ConstantValue初始化为最终值了,对常量字段的引用不会导致类进入“初始化”阶段。
ConstantValue: String 2222222
/*方法表的默认构造方法开始*/
public com.ikang.JVM.classfile.ClassFile();
//方法描述符
descriptor: ()V
//方法访问标记
flags: ACC_PUBLIC
//方法Code属性
Code:
//栈最大深度1,局部变量最大数量1,参数1
stack=1, locals=1, args_size=1
/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//行号表, 用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中字节码字节码指令左边的数字。一行可以对应多个字节码指令。
LineNumberTable:
line 4: 0
//局部变量表, start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置,Name就是变量的名字,slot就是这个变量在局部变量表中的槽位(槽位可复用,从0开始),name就是变量名称,Signatur局部变量的字段类型描述符;
//下面的含义就是名叫this的局部变量的字节码生命周期从头0到结尾5(不包括),占用第一个槽位(索引0),变量是ClassFile类型。
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
/*方法表的默认构造方法 结束*/
/*方法表的getk方法 开始*/
public int getK();
//方法描述符,很简单,表示返回值为int ,参数为空
descriptor: ()I
//访问标记 public
flags: ACC_PUBLIC
//Code属性,包括方法字节码,行号表,局部变量表
Code:
//栈最大深度1,局部变量最大数量1,参数1
stack=1, locals=1, args_size=1
/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
//行号表
LineNumberTable:
line 10: 0
//局部变量表,只有一个this变量
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
/*方法表的getk方法 结束*/
/*方法表的setK方法 开始*/
public void setK(int) throws java.lang.Exception;
//方法描述符,很简单,表示返回值为void ,参数为int
descriptor: (I)V
//访问标记 public
flags: ACC_PUBLIC
//Code属性,包括方法字节码,行号表,局部变量表,该方法还包括异常表、栈图
Code:
//栈最大深度2,局部变量最大数量4,参数2
stack=2, locals=4, args_size=2
/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
//异常表属性, 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。异常表是由try-catch语句生成的。具体结构在class文件结构那篇文章处有介绍。
Exception table:
from to target type
//第一行表示 0~5(不包括) 行出现的 IllegalStateException 异常,直接跳转到 8(astore_2) 行;java代码层面就是:如果try语句块中出现属于IllegalStateException或其子类的异常,则转到catch语句块处理。
0 5 8 Class java/lang/IllegalStateException
//第二行表示 0~5(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行,java代码层面就是:如果try语句块中出现不属于IllegalStateException或其子类的异常,则转到finally语句块处理。从这里可以看出,finally关键字的语义也是由异常表实现的。
0 5 16 any
//第三行表示 8~13(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行;java代码层面就是:如果catch语句块中出现任何异常,则转到finally语句块处理。
8 13 16 any
//行号表
LineNumberTable:
line 16: 0
line 20: 5
line 17: 8
line 18: 9
line 20: 13
line 19: 16
line 21: 19
//局部变量表,有3个局部变量
LocalVariableTable:
Start Length Slot Name Signature
9 4 2 e Ljava/lang/IllegalStateException;
0 20 0 this Lcom/ikang/JVM/classfile/ClassFile;
0 20 1 k I
//栈图,该属性不包含运行时所需的信息,仅仅用于用于加快Class文件的类型检验。
StackMapTable: number_of_entries = 3
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/IllegalStateException ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 2 /* same */
//列举异常,表示一个方法可能抛出的异常,通常是由方法的throws 关键字指定的,这和Code属性中的异常表不一样。
Exceptions:
throws java.lang.Exception
/*方法表的setK方法 结束*/
/*方法表的main方法 开始*/
public static void main(java.lang.String[]);
//方法描述符,很简单,表示返回值为void ,参数为String[]
descriptor: ([Ljava/lang/String;)V
//访问标记,public static
flags: ACC_PUBLIC, ACC_STATIC
//code属性
Code:
stack=0, locals=1, args_size=1
//由于没有代码,因此只有return一个字节码,表示方法的结束.
0: return
LineNumberTable:
line 24: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
//记录class文件的源文件 ClassFile.java
SourceFile: "ClassFile.java"
2.2.2.1 构造方法的字节码解析
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
左边的数字表示字节码偏移量(从0开始),字节码的偏移量和前面的字节码长度有关系,例如aload_0是第一个字节码,自然偏移量是0,它本身占一个字节,因此 invokespecial就从索引1开始,invokespecial本身占据一个字节,但是接收一个两个字节的无符号数参数,因此整个invokespecial指令占据3个字节,故后续的return指令位于偏移量4处。
在最开始时,局部变量表中具有一个变量this。局部变量表第0项为this引用,表示当前对象。在Java 中, 对千所有的非静态函数调用, 为了能顺利访问this对象,都会将对象的引用放置在局部变量表第0 个槽位,如下图:
执行第一条指令,aload_0,表示把局部变量第 1 个引用型局部变量推到操作数栈,即把this对象引用压入操作数栈的栈顶(操作数栈相当于虚拟机的工作区,主要用于字节码的执行——大多数指令都要从这里先压入原始数据,然后弹出数据,执行运算,然后把结果压回操作数栈,最后把结果赋给局部变量表的变量),如下图:
执行第二条指令,invokespecial, 表示对栈顶对象的私有、构造、父类方法进行调用,这些方法的特点是调用目标在编译时就确定了,对这些方法的调用又称为“解析”。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个方法的符号引用,表示将执行栈顶对象对应的该方法。
javap已经帮我们算出来了,该索引就是值为1的索引处的常量项,我们回去看看第一项常量,是< init >方法的符号引用。即调用实例初始化方法(< init >)来完成对象的初始化,就是this指定的对象的调用< init >方法完成初始化。
第二条指令执行完,即该类对象被初始化完毕,执行第三条指令,return,表示方法结束,并从当前方法返回 void,并清空栈空间数据包括操作数栈和局部变量表。
从上面的三条指令可以看出来,在无参构造方法调用了< init >方法对对象进行初始化, < init >方法用于将对象字段的零值初始化为指定值。
2.2.2.2 getK方法的字节码解析
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
在最开始时,同样局部变量表中具有一个变量this,操作数栈为空。
执行第一个指令,aload_0,表示把 局部变量第1个引用型局部变量推到操作数栈顶 ,即把this对象引用压入操作数栈的栈顶。
执行第二个指令,getfield,表示 获取指定对象的字段值,并将其值压入栈顶 。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个字段的符号引用,指令执行后,该字段的值将被取出,并压入到操作数栈顶。很明显,这里指向第二项常量,我们回去看看第二项常量,的确是k字段 的符号引用。
第二个指令执行完毕后如下图:
执行第三个指令,ireturn,表示结束方法并返回一个int 类型数据。很明显是将操作数栈的栈顶值返回(对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作)。
2.2.2.3 setK方法的字节码解析
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
//异常表属性, 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。异常表是由try-catch语句生成的。具体结构在class文件结构处有介绍。
Exception table:
from to target type
//第一行表示 0~5(不包括) 行出现的 IllegalStateException 异常,直接跳转到 8(astore_2) 行;java代码层面就是:如果try语句块中出现属于IllegalStateException或其子类的异常,则转到catch语句块处理。
0 5 8 Class java/lang/IllegalStateException
//第二行表示 0~5(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行,java代码层面就是:如果try语句块中出现不属于IllegalStateException或其子类的异常,则转到finally语句块处理。从这里可以看出,finally关键字的语义也是由异常表实现的。
0 5 16 any
//第三行表示 8~13(不包括)行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行;java代码层面就是:如果catch语句块中出现任何异常,则转到finally语句块处理。
8 13 16 any
该方法的字节码明显变多了,主要加了一些异常处理逻辑。不过一个一个看,也很简单。
执行第1个指令,aload_0,表示把局部变量第1个引用型局部变量推到操作数栈顶,即把this对象引用压入操作数栈的栈顶。
执行第2个指令,iload_1,表示把局部变量第2个int型局部变量推到操作数栈顶,即把k字段的值压入操作数栈的栈顶。
执行第3个指令,putfield,表示设置对象字段的值。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个字段的符号引用,指令执行后,操作数栈中的该字段所属的对象this和具体的值,将被弹出栈。putfield的参数指向常量池第二个常量,明显表示k字段。
执行第4个指令,goto,表示无条件分支跳转。 该指令接收一个2个字节长度的有符号参数,用于构建一个16 位有符号的分支偏移量。指令执行后,程序将会转到这个goto 指令之后的,由上述偏移量确定的目标地址上继续执行。这个目标地址必须处于goto 指令所在的方法之中。
很明显,后面的偏移量是19,这说明为k赋值之后,就可以执行return了。因为本代码finally块中没有定义代码,实际上如果finally中有代码,那么将会在每一个goto之前插入finally的字节码,这也从字节码的层面保证了finally语句块必须执行!
执行第5个指令,astore_2,表示。把栈顶引用型数值存入第3个局部变量表位置。 |
执行第6个指令,aload_2,表示把局部变量第3个引用型局部变量推到操作数栈。 |
执行第7个指令,invokevirtual,表示调用实例方法,依据实例的类型进行方法分派。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个方法的符号引用,这里指向第四个常量,我们返回查看常量池,发现是printStackTrace方法的符号引用。 |
实际上,上面表格部分的字节码,就是catch块中的内容。我们根据异常表,当发生IllegalStateException异常,将会跳转到astore_2,执行catch中的内容。
查看局部变量表可知,实际上astore_2是将IllegalStateException异常引用存放了局部变量表的第三个位置。
第6、7个指令实际上就是在调用printStackTrace方法。
执行第8个指令,goto,表示无条件分支跳转。这是第二个goto指令,在代码逻辑层面:表示catch执行完毕,finally语句块执行完毕(本finally中没有代码),方法结束。
第9个指令,astore_3, 表示把栈顶引用型数值存入第4个局部变量表位置。能够执行到这个指令,根据异常表,说明catch中出现了异常或者出现不属于IllegalStateException或其子类的异常,将该异常类型,放到第四个局部变量表位置。我们去查看局部变量表,并没有第四个异常变量,这说明这一段字节码的逻辑不一定会执行,在编译时还不确定该异常的类型。
第10个指令,aload_3, 表示把局部变量第4个引用型局部变量推到操作数栈。即将上面的异常,放到操作数栈。但是在该指令之前还会执行finally中的字节码。
第11个指令,athrow, 表示将栈顶的异常抛出。即将上面的未能使用catch捕获的异常抛出,并结束方法。
第12个指令,return,作为方法结束的标志。
到此,setk方法结束。
补充:
如果在finally中添加一个输出语句:System.out.println(11);那么字节码将会如下:
我们可以看到,finally中的字节码被插入到每一个可能的分支的最后,代码层面就表示:无论如何,finally中代码都将被执行,这是jvm在编译时为我们做出的字节码级别的保证。
3 jclasslib替代javap
实际上,现在通过jclasslib工具可以完全替代javap命令,jclasslib可用于用于打开class文件,而且是可视化的,效果更好一些。jclasslib还提供了修改jar包中的Class文件的API。
为什么先介绍比较麻烦的javap?因为javap是Java官方自带的,先了解原生的工具之后,再来使用jclasslib,我们将会更加的得心应手。
jclasslib的github地址:jclasslib。
3.1 idea安装jclasslib插件
- 使用 ALT+CTRL+S 打开setting
- 选择plugins(插件),搜索jclasslib,然后安装,重启
- 选择要打开的class文件,点击上面的view,点击show bytecode with jclasslib,在右侧就会出现可视化界面
- 右侧的可视化界面,可以看出来还是很简单的,这里就不介绍了。
3.2 直接安装jclasslib
当然有些开发者没有使用idea, github上这里也可以直接安装jclasslib。
安装好之后,将class文件放入jclasslib中,可以看到,界面和idea中的界面差不多。
如果由于某些神奇的原因,github上展示无法下载,可以使用本人提供的链接:jclasslib。
3.3 修改class文件字节码案例
3.3.1 准备对比输出
因为修改class文件的代码使用到了同名的类,为了不引起混淆,将原代码的类名改为ClassFile1,然后main方法添加如下代码:
public static void main(String[] args) {
System.out.println(J);
}
就是简单的打印常量J的值运行输出:2222222。
拿到class文件和源码放到一个路径下面。
在包路径开始处,运行cmd,尝试使用命令行带包执行class文件:
可见还是输出为2222222
3.3.2 修改class文件
我们此次作简单的修改,将输出2222222改成3333。
我们的jclasslib已经提供了修改class文件的API。在jclasslib的安装目录中找到lib目录,将下面的jar包拷贝到项目中:
编写代码:
public class ModifyClass {
public static void main(String[] args) throws Exception {
FileInputStream fis = null;
try {
//自己的class文件路径
String filePath = "J:\\Idea\\jvm\\src\\main\\java\\com\\ikang\\JVM\\classfile\\ClassFile1.class";
fis = new FileInputStream(filePath);
DataInput di = new DataInputStream(fis);
ClassFile cf = new ClassFile();
cf.read(di);
CPInfo[] infos = cf.getConstantPool();
//找到常量池46个位置,CONSTANT_Utf-8_info所以这里要用这个
ConstantUtf8Info uInfo = (ConstantUtf8Info) infos[46];
//设置新值
uInfo.setBytes("3333".getBytes());
//替换回去
infos[46] = uInfo;
cf.setConstantPool(infos);
File f = new File(filePath);
ClassFileWriter.writeToFile(f, cf);
} finally {
if (fis != null) {
fis.close();
}
}
}
}
为什么找到46个位置呢,因为要修改的J常量的“2222222”字面量值就是存在第46个常量中:
运行之后,再次使用java命令执行原来的class文件:
结果输出3333,修改成功。
4 附:字节码指令表
字节码指令根据功能、属性不同,可以分为11大类。下面附上字节码指令的分类,用于简单、临时查看,字节码指令的详细介绍,还需要查看官网的介绍。
4.1 Constants 常量相关
十进制 | 操作码 | 助记符 | 含义 |
00 | 0x00 | nop | 什么都不做 |
01 | 0x01 | aconst_null | 把 null 推到操作数栈 |
02 | 0x02 | iconst_m1 | 把 int 常量 –1 推到操作数栈 |
03 | 0x03 | iconst_0 | 把 int 常量 0 推到操作数栈 |
04 | 0x04 | iconst_1 | 把 int 常量 1 推到操作数栈 |
05 | 0x05 | iconst_2 | 把 int 常量 2 推到操作数栈 |
06 | 0x06 | iconst_3 | 把 int 常量 3 推到操作数栈 |
07 | 0x07 | iconst_4 | 把 int 常量 4 推到操作数栈 |
08 | 0x08 | iconst_5 | 把 int 常量 5 推到操作数栈 |
09 | 0x09 | lconst_0 | 把 long 常量 0 推到操作数栈 |
10 | 0x0A | lconst_1 | 把 long 常量 1 推到操作数栈 |
11 | 0x0B | fconst_0 | 把 float 常量 0 推到操作数栈 |
12 | 0x0C | fconst_1 | 把 float 常量 1 推到操作数栈 |
13 | 0x0D | fconst_2 | 把 float 常量 2 推到操作数栈 |
14 | 0x0E | dconst_0 | 把 double 常量 0 推到操作数栈 |
15 | 0x0F | dconst_1 | 把 double 常量 1 推到操作数栈 |
16 | 0x10 | bipush | 把单字节常量(-128~127)推到操作数栈 |
17 | 0x11 | sipush | 把 short 常量(-32768~32767)推到操作数栈 |
18 | 0x12 | ldc | 把常量池中的int,float,String型常量取出并推到操作数栈顶 |
19 | 0x13 | ldc_w | 把常量池中的int,float,String型常量取出并推到操作数栈顶(宽索引) |
20 | 0x14 | ldc2_w | 把常量池中的long,double型常量取出并推到操作数栈顶(宽索引) |
4.2 Loads 加载相关
十进制 | 操作码 | 助记符 | 含义 |
21 | 0x15 | iload | 把 int 型局部变量推到操作数栈 |
22 | 0x16 | lload | 把 long 型局部变量推到操作数栈 |
23 | 0x17 | fload | 把 float 型局部变量推到操作数栈 |
24 | 0x18 | dload | 把 double 型局部变量推到操作数栈 |
25 | 0x19 | aload | 把引用型局部变量推到操作数栈 |
26 | 0x1A | iload_0 | 把局部变量第 1 个 int 型局部变量推到操作数栈 |
27 | 0x1B | iload_1 | 把局部变量第 2 个 int 型局部变量推到操作数栈 |
28 | 0x1C | iload_2 | 把局部变量第 3 个 int 型局部变量推到操作数栈 |
29 | 0x1D | iload_3 | 把局部变量第 4 个 int 型局部变量推到操作数栈 |
30 | 0x1E | lload_0 | 把局部变量第 1 个 long 型局部变量推到操作数栈 |
31 | 0x1F | lload_1 | 把局部变量第 2 个 long 型局部变量推到操作数栈 |
32 | 0x20 | lload_2 | 把局部变量第 3 个 long 型局部变量推到操作数栈 |
33 | 0x21 | lload_3 | 把局部变量第 4 个 long 型局部变量推到操作数栈 |
34 | 0x22 | fload_0 | 把局部变量第 1 个 float 型局部变量推到操作数栈 |
35 | 0x23 | fload_1 | 把局部变量第 2 个 float 型局部变量推到操作数栈 |
36 | 0x24 | fload_2 | 把局部变量第 3 个 float 型局部变量推到操作数栈 |
37 | 0x25 | fload_3 | 把局部变量第 4 个 float 型局部变量推到操作数栈 |
38 | 0x26 | dload_0 | 把局部变量第 1 个 double 型局部变量推到操作数栈 |
39 | 0x27 | dload_1 | 把局部变量第 2 个 double 型局部变量推到操作数栈 |
40 | 0x28 | dload_2 | 把局部变量第 3 个 double 型局部变量推到操作数栈 |
41 | 0x29 | dload_3 | 把局部变量第 4 个 double 型局部变量推到操作数栈 |
42 | 0x2A | aload_0 | 把局部变量第 1 个引用型局部变量推到操作数栈 |
43 | 0x2B | aload_1 | 把局部变量第 2 个引用型局部变量推到操作数栈 |
44 | 0x2C | aload_2 | 把局部变量第 3 个引用型局部变量推到操作数栈 |
45 | 0x2D | aload_3 | 把局部变量第 4 个引用 型局部变量推到操作数栈 |
46 | 0x2E | iaload | 把 int 型数组指定索引的值推到操作数栈 |
47 | 0x2F | laload | 把 long 型数组指定索引的值推到操作数栈 |
48 | 0x30 | faload | 把 float 型数组指定索引的值推到操作数栈 |
49 | 0x31 | daload | 把 double 型数组指定索引的值推到操作数栈 |
50 | 0x32 | aaload | 把引用型数组指定索引的值推到操作数栈 |
51 | 0x33 | baload | 把 boolean或byte型数组指定索引的值推到操作数栈 |
52 | 0x34 | caload | 把 char 型数组指定索引的值推到操作数栈 |
53 | 0x35 | saload | 把 short 型数组指定索引的值推到操作数栈 |
4.3 Store 存储相关
十进制 | 操作码 | 助记符 | 含义 |
54 | 0x36 | istore | 把栈顶 int 型数值存入指定局部变量 |
55 | 0x37 | lstore | 把栈顶 long 型数值存入指定局部变量 |
56 | 0x38 | fstore | 把栈顶 float 型数值存入指定局部变量 |
57 | 0x39 | dstore | 把栈顶 double 型数值存入指定局部变量 |
58 | 0x3A | astore | 把栈顶引用型数值存入指定局部变量 |
59 | 0x3B | istore_0 | 把栈顶 int 型数值存入第 1 个局部变量 |
60 | 0x3C | istore_1 | 把栈顶 int 型数值存入第 2 个局部变量 |
61 | 0x3D | istore_2 | 把栈顶 int 型数值存入第 3 个局部变量 |
62 | 0x3E | istore_3 | 把栈顶 int 型数值存入第 4 个局部变量 |
63 | 0x3F | lstore_0 | 把栈顶 long 型数值存入第 1 个局部变量 |
64 | 0x40 | lstore_1 | 把栈顶 long 型数值存入第 2 个局部变量 |
65 | 0x41 | lstore_2 | 把栈顶 long 型数值存入第 3 个局部变量 |
66 | 0x42 | lstore_3 | 把栈顶 long 型数值存入第 4 个局部变量 |
67 | 0x43 | fstore_0 | 把栈顶 float 型数值存入第 1 个局部变量 |
68 | 0x44 | fstore_1 | 把栈顶 float 型数值存入第 2 个局部变量 |
69 | 0x45 | fstore_2 | 把栈顶 float 型数值存入第 3 个局部变量 |
70 | 0x46 | fstore_3 | 把栈顶 float 型数值存入第 4 个局部变量 |
71 | 0x47 | dstore_0 | 把栈顶 double 型数值存入第 1 个局部变量 |
72 | 0x48 | dstore_1 | 把栈顶 double 型数值存入第 2 个局部变量 |
73 | 0x49 | dstore_2 | 把栈顶 double 型数值存入第 3 个局部变量 |
74 | 0x4A | dstore_3 | 把栈顶 double 型数值存入第 4 个局部变量 |
75 | 0x4B | astore_0 | 把栈顶 引用 型数值存入第 1 个局部变量 |
76 | 0x4C | astore_1 | 把栈顶 引用 型数值存入第 2 个局部变量 |
77 | 0x4D | astore_2 | 把栈顶 引用 型数值存入第 3 个局部变量 |
78 | 0x4E | astore_3 | 把栈顶 引用 型数值存入第 4 个局部变量 |
79 | 0x4F | iastore | 把栈顶 int 型数值存入数组指定索引位置 |
80 | 0x50 | lastore | 把栈顶 long 型数值存入数组指定索引位置 |
81 | 0x51 | fastore | 把栈顶 float 型数值存入数组指定索引位置 |
82 | 0x52 | dastore | 把栈顶 double 型数值存入数组指定索引位置 |
83 | 0x53 | aastore | 把栈顶 引用 型数值存入数组指定索引位置 |
84 | 0x54 | bastore | 把栈顶 boolean or byte 型数值存入数组指定索引位置 |
85 | 0x55 | castore | 把栈顶 char 型数值存入数组指定索引位置 |
86 | 0x56 | sastore | 把栈顶 short 型数值存入数组指定索引位置 |
4.4 Stack 栈相关
十进制 | 操作码 | 助记符 | 含义 |
87 | 0x57 | pop | 把栈顶数值弹出(非long,double数值) |
88 | 0x58 | pop2 | 把栈顶的一个long或double值弹出,或弹出2个其他类型数值 |
89 | 0x59 | dup | 复制栈顶数值并把数值入栈 |
90 | 0x5A | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
91 | 0x5B | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
92 | 0x5C | dup2 | 复制栈顶一个(long 或double 类型的)或两个(其它)数值并将复制值压入栈顶 |
93 | 0x5D | dup2_x1 | dup_x1 指令的双倍版本 |
94 | 0x5E | dup2_x2 | dup_x2 指令的双倍版本 |
95 | 0x5F | swap | 把栈顶端的两个数的值交换(数值不能是long 或double 类型< td >的) |
4.5 Math 运算相关
Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会使用 NaN 值来表示。所有使用 NaN 值作为操作数的算术操作,结果都会返回 NaN。
十进制 | 操作码 | 助记符 | 含义 |
96 | 0x60 | iadd | 把栈顶两个 int 型数值相加并将结果入栈 |
97 | 0x61 | ladd | 把栈顶两个 long 型数值相加并将结果入栈 |
98 | 0x62 | fadd | 把栈顶两个 float 型数值相加并将结果入栈 |
99 | 0x63 | dadd | 把栈顶两个 double 型数值相加并将结果入栈 |
100 | 0x64 | isub | 把栈顶两个 int 型数值相减并将结果入栈 |
101 | 0x65 | lsub | 把栈顶两个 long 型数值相减并将结果入栈 |
102 | 0x66 | fsub | 把栈顶两个 float 型数值相减并将结果入栈 |
103 | 0x67 | dsub | 把栈顶两个 double 型数值相减并将结果入栈 |
104 | 0x68 | imul | 把栈顶两个 int 型数值相乘并将结果入栈 |
105 | 0x69 | lmul | 把栈顶两个 long 型数值相乘并将结果入栈 |
106 | 0x6A | fmul | 把栈顶两个 float 型数值相乘并将结果入栈 |
107 | 0x6B | dmul | 把栈顶两个 double 型数值相乘并将结果入栈 |
108 | 0x6C | idiv | 把栈顶两个 int 型数值相除并将结果入栈 |
109 | 0x6D | ldiv | 把栈顶两个 long 型数值相除并将结果入栈 |
110 | 0x6E | fdiv | 把栈顶两个 float 型数值相除并将结果入栈 |
111 | 0x6F | ddiv | 把栈顶两个 double 型数值相除并将结果入栈 |
112 | 0x70 | irem | 把栈顶两个 int 型数值模运算并将结果入栈 |
113 | 0x71 | lrem | 把栈顶两个 long 型数值模运算并将结果入栈 |
114 | 0x72 | frem | 把栈顶两个 float 型数值模运算并将结果入栈 |
115 | 0x73 | drem | 把栈顶两个 double 型数值模运算并将结果入栈 |
116 | 0x74 | ineg | 把栈顶 int 型数值取负并将结果入栈 |
117 | 0x75 | lneg | 把栈顶 long 型数值取负并将结果入栈 |
118 | 0x76 | fneg | 把栈顶 float 型数值取负并将结果入栈 |
119 | 0x77 | dneg | 把栈顶 double 型数值取负并将结果入栈 |
120 | 0x78 | ishl | 把 int 型数左移指定位数并将结果入栈 |
121 | 0x79 | lshl | 把 long 型数左移指定位数并将结果入栈 |
122 | 0x7A | ishr | 把 int 型数右移指定位数并将结果入栈(有符号) |
123 | 0x7B | lshr | 把 long 型数右移指定位数并将结果入栈(有符号) |
124 | 0x7C | iushr | 把 int 型数右移指定位数并将结果入栈(无符号) |
125 | 0x7D | lushr | 把 long 型数右移指定位数并将结果入栈(无符号) |
126 | 0x7E | iand | 把栈顶两个 int 型数值 按位与 并将结果入栈 |
127 | 0x7F | land | 把栈顶两个 long 型数值 按位与 并将结果入栈 |
128 | 0x80 | ior | 把栈顶两个 int 型数值 按位或 并将结果入栈 |
129 | 0x81 | lor | 把栈顶两个 long 型数值 按或与 并将结果入栈 |
130 | 0x82 | ixor | 把栈顶两个 int 型数值 按位异或 并将结果入栈 |
131 | 0x83 | lxor | 把栈顶两个 long 型数值 按位异或 并将结果入栈 |
132 | 0x84 | iinc | 把指定 int 型增加指定值 |
4.6 Conversions 转换相关
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作。
Java 虚拟机直接支持(即转换时无需显示的转换指令)小范围类型向大范围类型的安全转换,但在处理窄化类型转换时,必须显式使用转换指令来完成。
十进制 | 操作码 | 助记符 | 含义 |
133 | 0x85 | i2l | 把栈顶 int 强转 long 并入栈 |
134 | 0x86 | i2f | 把栈顶 int 强转 float 并入栈 |
135 | 0x87 | i2d | 把栈顶 int 强转 double 并入栈 |
136 | 0x88 | l2i | 把栈顶 long 强转 int 并入栈 |
137 | 0x89 | l2f | 把栈顶 long 强转 float 并入栈 |
138 | 0x8A | l2d | 把栈顶 long 强转 double 并入栈 |
139 | 0x8B | f2i | 把栈顶 float 强转 int 并入栈 |
140 | 0x8C | f2l | 把栈顶 float 强转 long 并入栈 |
141 | 0x8D | f2d | 把栈顶 float 强转 double 并入栈 |
142 | 0x8E | d2i | 把栈顶 double 强转 int 并入栈 |
143 | 0x8F | d2l | 把栈顶 double 强转 long 并入栈 |
144 | 0x90 | d2f | 把栈顶 double 强转 float 并入栈 |
145 | 0x91 | i2b | 把栈顶 int 强转 byte 并入栈 |
146 | 0x92 | i2c | 把栈顶 int 强转 char 并入栈 |
147 | 0x93 | i2s | 把栈顶 int 强转 short 并入栈 |
4.7 Comparisons 比较相关
十进制 | 操作码 | 助记符 | 含义 |
148 | 0x94 | lcmp | 比较栈顶两long 型数值大小,并将结果(1,0,-1)压入栈顶 |
149 | 0x95 | fcmpl | 比较栈顶两float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将-1 压入栈顶 |
150 | 0x96 | fcmpg | 比较栈顶两float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将1 压入栈顶 |
151 | 0x97 | dcmpl | 比较栈顶两double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将-1 压入栈顶 |
152 | 0x98 | dcmpg | 比较栈顶两double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将1 压入栈顶 |
153 | 0x99 | ifeq | 当栈顶 int 型数值等于0时,跳转 |
154 | 0x9A | ifne | 当栈顶 int 型数值不等于0时,跳转 |
155 | 0x9B | iflt | 当栈顶 int 型数值小于0时,跳转 |
156 | 0x9C | ifge | 当栈顶 int 型数值大于等于0时,跳转 |
157 | 0x9D | ifgt | 当栈顶 int 型数值大于0时,跳转 |
158 | 0x9E | ifle | 当栈顶 int 型数值小于等于0时,跳转 |
159 | 0x9F | if_icmpeq | 比较栈顶两个 int 型数值,等于0时,跳转 |
160 | 0xA0 | if_icmpne | 比较栈顶两个 int 型数值,不等于0时,跳转 |
161 | 0xA1 | if_icmplt | 比较栈顶两个 int 型数值,小于0时,跳转 |
162 | 0xA2 | if_icmpge | 比较栈顶两个 int 型数值,大于等于0时,跳转 |
163 | 0xA3 | if_icmpgt | 比较栈顶两个 int 型数值,大于0时,跳转 |
164 | 0xA4 | if_icmple | 比较栈顶两个 int 型数值,小于等于0时,跳转 |
165 | 0xA5 | if_acmpeq | 比较栈顶两个 引用 型数值,相等时跳转 |
166 | 0xA6 | if_acmpne | 比较栈顶两个 引用 型数值,不相等时跳转 |
4.8 Control 控制相关
控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。
十进制 | 操作码 | 助记符 | 含义 |
167 | 0xA7 | goto | 无条件分支跳转 |
168 | 0xA8 | jsr | 跳转至指定16 位offset(bit) 位置,并将jsr 下一条指令地址压入栈顶 |
169 | 0xA9 | ret | 返回至局部变量指定的index 的指令位置(一般与jsr,jsr_w联合使用) |
170 | 0xAA | tableswitch | 用于switch 条件跳转,case 值连续(可变长度指令) |
171 | 0xAB | lookupswitch | 用于switch 条件跳转,case 值不连续(可变长度指令) |
172 | 0xAC | ireturn | 结束方法,并返回一个int 类型数据 |
173 | 0xAD | lreturn | 从当前方法返回 long |
174 | 0xAE | freturn | 从当前方法返回 float |
175 | 0xAF | dreturn | 从当前方法返回 double |
176 | 0xB0 | areturn | 从当前方法返回 对象引用 |
177 | 0xB1 | return | 从当前方法返回 void |
4.9 references 引用、方法、异常、同步相关
十进制 | 操作码 | 助记符 | 含义 |
178 | 0xB2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
179 | 0xB3 | putstatic | 为类的静态域赋值 |
180 | 0xB4 | getfield | 获取指定类的实例域(对象的字段值),并将其值压入栈顶 |
181 | 0xB5 | putfield | 为指定的类的实例域赋值 |
182 | 0xB6 | invokevirtual | 调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),是Java语言中最常见的方法分派方式。 |
183 | 0xB7 | invokespecial | 调用一些需要特殊处理的实例方法,包括实例初始化方法()、私有方法和父类方法。这三类方法的调用对象在编译时就可以确定。 |
184 | 0xB8 | invokestatic | 调用静态方法 |
185 | 0xB9 | invokeinterface | 调用接口方法调,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。 |
186 | 0xBA | invokedynamic | 调用动态链接方法(该指令是指令是Java SE 7 中新加入的)。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 |
187 | 0xBB | new | 创建一个对象,并将其引用值压入栈顶 |
188 | 0xBC | newarray | 创建一个指定原始类型(如int、float、char……)的数组,并将其引用值压入栈顶 |
189 | 0xBD | anewarray | 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 |
190 | 0xBE | arraylength | 获得数组的长度值并压入栈顶 |
191 | 0xBF | athrow | 将栈顶的异常直接抛出。Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,并且,在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。 |
192 | 0xC0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
193 | 0xC1 | instanceof | 检验对象是否是指定的类的实例,如果是将1 压入栈顶,否则将0 压入栈顶 |
194 | 0xC2 | monitorenter | 获取对象的monitor,用于同步块或同步方法 |
195 | 0xC3 | monitorexit | 释放对象的monitor,用于同步块或同步方法 |
Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
**方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。**虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 方法标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。
4.10 Extended 扩展相关
十进制 | 操作码 | 助记符 | 含义 |
196 | 0xC4 | wide | 扩展访问局部变量表的索引宽度 |
197 | 0xC5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
198 | 0xC6 | ifnull | 为 null 时跳转 |
199 | 0xC7 | ifnonnull | 非 null 时跳转 |
200 | 0xC8 | goto_w | 无条件跳转(宽索引) |
201 | 0xC9 | jsr_w | 跳转指定32bit偏移位置,并将jsr_w下一条指令地址入栈 |
4.11 Reserved 保留指令
十进制 | 操作码 | 助记符 | 含义 |
202 | 0xCA | breakpoint | 调试时的断点 |
254 | 0xFE | impdep1 | 用于在特定硬件中使用的语言后门 |
255 | 0xFF | impdep2 | 用于在特定硬件中使用的语言后门 |
5 参考和学习
- JVM规范 Java SE8官方文档
- JVM规范中《操作码助记符表》
- JVM规范中《JVM指令集》介绍(包括操作码对应的操作数)
- 《Java虚拟机规范》
- 《深入理解Java虚拟机》
- 《实战Java虚拟机》
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
转载自:https://juejin.cn/post/7027707475503611940