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 getKsetK方法的字节码

         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插件

  1. 使用 ALT+CTRL+S 打开setting
  2. 选择plugins(插件),搜索jclasslib,然后安装,重启
    在这里插入图片描述
  3. 选择要打开的class文件,点击上面的view,点击show bytecode with jclasslib,在右侧就会出现可视化界面
    在这里插入图片描述
  4. 右侧的可视化界面,可以看出来还是很简单的,这里就不介绍了。
    在这里插入图片描述

3.2 直接安装jclasslib

  当然有些开发者没有使用idea, github上这里也可以直接安装jclasslib。
  下载地址:https://github.com/ingokegel/jclasslib/releases
  安装好之后,将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学习博客!

发布了72 篇原创文章 · 获赞 135 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/105400912