jvm常见字节码指令

一个class文件的完整格式

  • 使用javap命令可以将一个二进制的字节码文件反编译为人类容易阅读的形式。分析class文件最关键弄清楚常量池每个常量含义和方法中每个指令含义。
  • 对于如下java代码,使用javac我们将其编译为class文件
public class Test {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}
  • 然后使用命令 javap -v Test.class 可以得到如下信息,它包括了类基本信息 常量池 方法信息
Classfile /E:/projectstudy/javasestudy/src/main/java/Test.class //路径
  Last modified 2020-9-25; size 466 bytes //修改时间和大小
  MD5 checksum 575e1dd61e5f0715a5c376086734f50a //md5校验签名
  Compiled from "Test.java" //编译源文件
public class Test //类的全路径名称
  minor version: 0 
  major version: 52 //表示jdk8
  flags: ACC_PUBLIC, ACC_SUPER //类的访问修饰符是public
Constant pool: //常量池信息
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V  表示了一个方法引用,引用了第6项和第17项内容
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream; 表示一个成员变量引用
   #3 = String             #20            // hello 表示一个字符串引用
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            // Test
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               Exceptions
  #14 = Class              #25            // java/lang/InterruptedException
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Class              #26            // java/lang/System
  #19 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #20 = Utf8               hello
  #21 = Class              #29            // java/io/PrintStream
  #22 = NameAndType        #30:#31        // println:(Ljava/lang/String;)V
  #23 = Utf8               Test
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/InterruptedException
  #26 = Utf8               java/lang/System
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               (Ljava/lang/String;)V
{ //大括号里面是方法信息
  public Test(); //构造方法信息
    descriptor: ()V //方法参数
    flags: ACC_PUBLIC //方法修饰符
    Code: //方法代码部分
      stack=1, locals=1, args_size=1 //栈的深度 局部变量表的长度 参数的长度
         0: aload_0 //把局部变量第0项加载到操作数栈
         1: invokespecial #1   //调用常量池中第一项的方法 也就是object的构造方法  Method java/lang/Object."<init>":()V
         4: return //返回
      LineNumberTable:
        line 2: 0 //2表示java源代码的行号 0表示字节码的行号
  //main方法信息
  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   //从常量池获取静态变量System.out  Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3   //加载常量池中的字符串hello  String hello
         5: invokevirtual #4   //调用println方法 Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return //返回
      LineNumberTable:
        line 5: 0 //源代码第5行对应字节码第0行
        line 6: 8 //源代码第6行对应字节码第8行
}
  • 常用的jvm字节码指令
将一个数压入到操作数栈
bipush    将单字节的常量值(-128~127)推送至栈顶
sipush    将一个短整型常量值(-32768~32767)推送至栈顶
ldc    将int, float或String型常量值从常量池中推送至栈顶
ldc_w    将int, float或String型常量值从常量池中推送至栈顶(宽索引)
ldc2_w    将longdouble型常量值从常量池中推送至栈顶(宽索引)

将本地变量表中一个数压入到栈顶
iload    将指定的int型本地变量推送至栈顶
lload    将指定的long型本地变量推送至栈顶
fload    将指定的float型本地变量推送至栈顶
dload    将指定的double型本地变量推送至栈顶
aload    将指定的引用类型本地变量推送至栈顶
---
iconst_0    将int0推送至栈顶
iconst_1    将int1推送至栈顶
iconst_2    将int2推送至栈顶
iconst_3    将int3推送至栈顶

将操作数栈的栈顶存入本地变量表
istore    将栈顶int型数值存入指定本地变量
lstore    将栈顶long型数值存入指定本地变量
fstore    将栈顶float型数值存入指定本地变量
dstore    将栈顶double型数值存入指定本地变量
astore    将栈顶引用型数值存入指定本地变量


计算相关指令
iadd    将栈顶两int型数值相加并将结果压入栈顶
isub    将栈顶两int型数值相减并将结果压入栈顶
imul    将栈顶两int型数值相乘并将结果压入栈顶
idiv    将栈顶两int型数值相除并将结果压入栈顶
irem    将栈顶两int型数值作取模运算并将结果压入栈顶
ineg    将栈顶int型数值取负并将结果压入栈顶
ishl    将int型数值左移位指定位数并将结果压入栈顶
ishr    将int型数值右(符号)移位指定位数并将结果压入栈顶
iushr    将int型数值右(无符号)移位指定位数并将结果压入栈顶
---
iinc    将指定int型变量增加指定值(i++, i--, i+=2)
i2l     将栈顶int型数值强制转换成long型数值并将结果压入栈顶
i2f     将栈顶int型数值强制转换成float型数值并将结果压入栈顶
i2d     将栈顶int型数值强制转换成double型数值并将结果压入栈顶
iinc    将指定int型变量增加指定值(在局部变量表直接操作)

返回指令
ireturn    从当前方法返回int
areturn    从当前方法返回对象引用
return    从当前方法返回void

 成员与方法指令
getstatic    获取指定类的静态域,并将其值压入栈顶
putstatic    为指定的类的静态域赋值
getfield    获取指定类的实例域,并将其值压入栈顶
putfield    为指定的类的实例域赋值
invokevirtual    调用实例方法  动态绑定
invokespecial    调用超类构造方法,实例初始化方法,私有方法  静态绑定
invokestatic    调用静态方法 静态绑定
invokeinterface 调用接口方法
invokedynamic  调用动态链接方法

创建对象
new   创建一个对象,并将其引用值压入栈顶
newarray    创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
anewarray    创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶

条件判断相关指令
ifeq    当栈顶int型数值等于0时跳转
ifne    当栈顶int型数值不等于0时跳转
iflt    当栈顶int型数值小于0时跳转
ifge    当栈顶int型数值大于等于0时跳转
ifgt    当栈顶int型数值大于0时跳转
ifle    当栈顶int型数值小于等于0时跳转
if_icmpeq    比较栈顶两int型数值大小,当结果等于0时跳转
if_icmpne    比较栈顶两int型数值大小,当结果不等于0时跳转
if_icmplt    比较栈顶两int型数值大小,当结果小于0时跳转
if_icmpge    比较栈顶两int型数值大小,当结果大于等于0时跳转
if_icmpgt    比较栈顶两int型数值大小,当结果大于0时跳转
if_icmple    比较栈顶两int型数值大小,当结果小于等于0时跳转
if_acmpeq    比较栈顶两引用型数值,当结果相等时跳转
if_acmpne    比较栈顶两引用型数值,当结果不相等时跳转
goto    无条件跳转

赋值语句的字节码解读

  • 分析如下Java代码
public static void main(String[] args) {
    int a = 10;
    int b = Short.MAX_VALUE+1;
    int c = a + b;
    System.out.println(c);
}
  • 对应字节码如下
Constant pool:
   #1 = Methodref          #7.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #20.#21        // java/io/PrintStream.println:(I)V
   #6 = Class              #22            // Test
   #7 = Class              #23            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               Test.java
  #16 = NameAndType        #8:#9          // "<init>":()V
  #17 = Utf8               java/lang/Short
  #18 = Class              #24            // java/lang/System
  #19 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(I)V
  #22 = Utf8               Test
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (I)V
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10  //将一个byte范围的数压入到操作数栈
         2: istore_1          //操作数栈顶出栈并存放到局部变量表1的位置
         3: ldc           #3  // 把常量池三号数据32768取出来存放到操作数栈 Short.MAX_VALUE+1是在编译器计算好了的
         5: istore_2         //栈顶弹出数据存入局部变量表2的位置
         6: iload_1        //把局部变量表1的值读入操作数栈
         7: iload_2        //把局部变量表2的值读入操作数栈
         8: iadd       //弹出操作数栈中两个变量求和,并把结果存入操作数栈
         9: istore_3   栈顶弹出数据存入局部变量表3的位置
        10: getstatic     #4    //获取常量池中 System.out 对象引用放入操作数栈
        13: iload_3    //把局部变量表3的值读入操作数栈
        14: invokevirtual #5   //调用println方法打印结果
        17: return
}

i++与++i的字节码解读

  • 通过底层字节码分析i++与++i的区别
  • 测试代码如下
public static void fun1(){
    int i = 1;
    int a = i++;
}

public static void fun2(){
    int i = 1;
    int a = ++i;
}
  • 字节码中常量池部分
Classfile /E:/projectstudy/javasestudy/src/main/java/Test.class
  Last modified 2020-9-26; size 377 bytes
  MD5 checksum 0c1587c50ee803f5109d2072f28e0dba
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // Test
   #3 = Class              #16            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               fun1
  #11 = Utf8               fun2
  #12 = Utf8               SourceFile
  #13 = Utf8               Test.java
  #14 = NameAndType        #4:#5          // "<init>":()V
  #15 = Utf8               Test
  #16 = Utf8               java/lang/Object
  • 方法fun1
public static void fun1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: bipush        100 //将100压入操作数栈
         2: istore_0      //将操作数栈栈顶存入本地变量表0
         3: iload_0       //从本地变量表0加载数据到操作数栈
         4: iinc          0, 1 //将本地变量表0号元素100增加1
         7: istore_1    //将栈顶元素100存入本地变量1的位置,此时本地变量1的位置就是100
         8: return
  • 方法fun2
public static void fun2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: bipush        100 //将100压入操作数栈
         2: istore_0       //将操作数栈栈顶存入本地变量表0
         3: iinc          0, 1 //将本地变量表0号元素100增加1
         6: iload_0    //从本地变量表0加载数据到操作数栈
         7: istore_1   //将栈顶元素101存入本地变量1的位置,此时本地变量1的位置就是101
         8: return
  • 就是因为load指令与iinc指令顺序不同造成了最终结果不同。

条件判断的字节码解读

  • 分析如下代码
public static void fun1(){
    int a = 100;
    if(a==100)
        System.out.println(1);
    else
        System.out.println(2);
}
  • 字节码如下
public static void fun1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: bipush        100  //将100压入操作数栈
         2: istore_0        //将操作数栈出栈并存放到局部变量表0中
         3: iload_0         //从局部变量表加载0号元素到操作数栈
         4: bipush        100 //将100压入操作数栈
         6: if_icmpne     19  //比较栈顶两个元素是否相等  如果相等往后指向,如果不相等跳转到19行开始执行
         9: getstatic     #2     //开始执行if中的打印
        12: iconst_1             //打印参数1加载到操作数栈
        13: invokevirtual #3      // 调用 println方法
        16: goto          26    //表示if语句执行完了直接跳到26行
        19: getstatic     #2     //开始执行else中的打印
        22: iconst_2            //打印参数2加载到操作数栈
        23: invokevirtual #3       
        26: return

循环控制的字节码解读

  • 分析如下代码
public static void fun1(){
    int a = 0;
    while(a<10)
        a++;
}
  • 对应字节码如下
public static void fun1();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=1, args_size=0
       0: iconst_0 //将0压入操作数栈
       1: istore_0 //将操作数栈出栈并存放到局部变量表0中
       2: iload_0 //从局部变量表加载0号元素到操作数栈
       3: bipush        10 //将10压入操作数栈
       5: if_icmpge     14 //比较栈顶两int型数值大小(0是否大于等于10),当结果大于等于0时跳转到14位置
       8: iinc          0, 1 //本地变量表0的元素+1
      11: goto          2 //返回到第二行执行
      14: return

静态代码块底层原理

  • java编译器会将一个类中的所有静态代码块和静态变量赋值语句根据顺序全部合并起来形成一个cinit方法在类第一次加载时执行。
  • 分析如下代码
public class Test {
//    public static Logger log = LoggerFactory.getLogger(Test.class);
    static int a = 10;
    static {
        a = 20;
    }
    static int b = 100;
    static {
        b = 200;
    }
}
  • 字节码如下
static int a;
    descriptor: I
    flags: ACC_STATIC

static int b;
    descriptor: I
    flags: ACC_STATIC

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10 //10入栈
         2: putstatic     #2  //为静态成员a赋值 Field a:I
         5: bipush        20 //20入栈
         7: putstatic     #2  //为静态成员a赋值 Field a:I
        10: bipush        100 //100入栈
        12: putstatic     #3   //为静态成员b赋值  Field b:I
        15: sipush        200 //200入栈
        18: putstatic     #3    //为静态成员b赋值  Field b:I
        21: return

构造代码块底层原理

  • Java编译器会将一个类中所有的构造代码块 普通成员赋值 构造方法 合并为一个方法在类实例化时调用
  • 分析如下代码
public class Test {
    int a = 10;
    {
        a = 20;
    }
    Test(){
        a = 30;
    }
}
  • 对应字节码如下
int a;
  descriptor: I
  flags:

Test();
  descriptor: ()V
  flags:
  Code:
    stack=2, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // 调用父类无参构造
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // 将10赋给a
      10: aload_0
      11: bipush        20
      13: putfield      #2                  // 将20赋给a
      16: aload_0
      17: bipush        30
      19: putfield      #2                  // 将30赋给a
      22: return

实例对象调用静态方法的底层分析

  • 我们知道实例对象可以调用类所有的静态方法,那么实例对象调用静态方法和类直接调用静态方法有什么不同吗?
  • 分析如下代码
public class Test {
    public static void fun(){}
    public static void main(String[] args) {
          new Test().fun();
    }
}
  • 对应字节码指令如下
Code:
  stack=2, locals=2, args_size=1
     0: new           #2           // 创建一个test对象,将其引用放入操作数栈
     3: dup                         //复制操作数栈顶值,并将其压入栈顶
     4: invokespecial #3            // 调用构造方法
     7: astore_1                   //将栈顶元素存放到本地变量表1
     8: aload_1                   //将本地变量表1的元素加载到操作数栈
     9: pop                       //将操作数栈的元素出栈
    10: invokestatic  #4          // 从常量池找到静态方法fun调用
    13: return
  • 可以发现,使用实例对象调用静态方法时,先使用load将对象引用加载到操作数栈,然后将其弹出,再根据常量池数据找到静态方法调用。也就是说 load与pop指令完全是浪费的,没有任何意义。它比类直接调用静态方法多了两个指令,也就增加了一些性能开销。

catch语句的字节码指令分析

  • 分析如下try-catch语句
public static void main(String[] args) {
    int i = 0;
    try{
        i = 10;
    }catch (Exception e){
        i = 20;
    }
}
  • 对应字节码指令如下 会使用一个Exception table来进行异常对象的匹配规则
Code:
  stack=1, locals=3, args_size=1
     0: iconst_0  //将0放入操作数栈
     1: istore_1  //将操作数栈栈顶元素存入本地变量表1
     2: bipush        10 //将10放入操作数栈
     4: istore_1         //将操作数栈栈顶元素存入本地变量表1
     5: goto          12 //如果没有匹配异常,跳到12行字节码执行
     8: astore_2          //如果匹配到异常,进行i=20的赋值操作
     9: bipush        20
    11: istore_1
    12: return
  Exception table: //异常对象表
     from    to  target type //含义为,检查2-5之间的指令是否匹配到了Exception异常,如果匹配到了从第八行指令开始执行
         2     5     8   Class java/lang/Exception
  • 分析如下try-catch-finally语句
public static void main(String[] args) {
    int i = 0;
    try{
        i = 10;
    }catch (Exception e){
        i = 20;
    }finally {
        i = 30;
    }
}
  • 对应字节码如下 可以看到,会将finally语句的代码分发到每一个可能执行的分支,保证finally一定会执行。
Code:
  stack=1, locals=4, args_size=1
     0: iconst_0    //i=0操作
     1: istore_1
     2: bipush        10 //i=10操作
     4: istore_1
     5: bipush        30 //i=30操作
     7: istore_1
     8: goto          27 //如果没有匹配异常,跳转到27行
    11: astore_2     
    12: bipush        20 //如果匹配到异常,进行i=20操作
    14: istore_1
    15: bipush        30 //i=30操作
    17: istore_1
    18: goto          27 //catch语句执行结束,也会跳转到27行
    21: astore_3         //如果发生了异常,但是异常不能被catch匹配会执行这里的代码 暂存异常对象
    22: bipush        30 //i=30操作
    24: istore_1
    25: aload_3        //抛出没有匹配的异常对象
    26: athrow
    27: return
  Exception table:
     from    to  target type
         2     5    11   Class java/lang/Exception  //如果匹配到Exception执行11行代码
         2     5    21   any   //try语句中出现了不能匹配的异常,执行21行代码
        11    15    21   any   //catch语句中出现了不能匹配的异常,执行21行代码

synchronized语句的字节码分析

  • 分析如下代码
public static void main(String[] args) {
    synchronized (Test.class){
        int a = 10;
    }
}
  • 对应字节码如下 可以看到其实利用了异常对象表来控制整个流程,保证无论是否发生异常都会正常解锁
Code:
  stack=2, locals=4, args_size=1
     0: ldc           #2    // 将test类对象加入操作数栈
     2: dup                //复制对象引用
     3: astore_1           //将test类对象存到本地变量表1
     4: monitorenter       //将test的类对象进行加锁操作
     5: bipush        10  //执行 a=10的赋值操作
     7: istore_2
     8: aload_1           //将本地变量表1的对象加载到操作数栈
     9: monitorexit      //然后执行解锁操作
    10: goto          18 //正常情况下就会跳转到18行执行
    13: astore_3      //如果发生异常,执行此处指令进行解锁操作
    14: aload_1
    15: monitorexit
    16: aload_3
    17: athrow
    18: return
  Exception table: //异常对象表来控制流程
     from    to  target type
         5    10    13   any //如果5-10之间匹配到任意异常,跳到13行执行
        13    16    13   any //如果13-16之间匹配到任意异常,跳到13行执行
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值