Java 虚拟机(一):Java 字节码

2019-05-14  本文已影响0人  yxhuang

Java 字节码

Java 字节码是 JVM 里面指令的型式, Java 的源码经过 Java 编译器会形成 Java 字节码,这的字节码才能在 Java 虚拟机中运行。


java_byte_1.png

一、栈基架构

一个虚拟机有基于栈虚拟机(Stack based Virtual Machine)和 基于寄存器虚拟机(Register based Virtual Machine)之法, 它们的差别可以看这里

Java 的虚拟机是基于栈的, 它包含 PC 寄存器、 JVM 栈、堆和方法区


java_byte_2.png

注:图片来自于 极客时间专栏《深入拆解 Java 虚拟机》

JVM 的栈是由一系列的 Frame 组成的,它包含

java_byte_3.png

注:图片来自JVM 内部原理(六)— Java 字节码基础之一

二、JVM 数据类型

JVM 定义的数据类型有:

类型 位数
byte 8位
short 16位
int 32 位
long 64 位
char 16 位
float 32 位 单精度
double 64 位 双精度

三、指令

Java 字节码的指令有一个 JVM 数据类型前缀和操作名组成,例如 idd 指令是由 "i" 表示 int 类型数据和 “add"相加操作组成,因此 iadd 表示对 int 类型数值求和。

根据指令的性质,可以分为以下类型:

下面有详细的说明

1.加载和存储指令

加载和存储指令用于数据在栈帧(Frame)中的局部变量数组(Local Variables)和操作栈(Operand Stack)之间来回传输;

具体为:

例如:
例子1:
Calc.java 文件

public class Calc {
    public int calc(){
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b ) * c;
    }
}

通过命令

javac Calc.java

会生成 Calc.class 文件
然后在通过命令 javap 可以看到相应的字节码

javap -v Calc.class

相应的字节码

  public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn

执行过程

java_byte_4.png java_byte_5.png java_byte_6.png

注:图片来自于周志明《深入理解 Java 虚拟机》

2.运算指令

运算指令用于对两个操作栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
分为两类:对整形数据进行运算的指令和对浮点型数据进行运算的指令。

上面例子1中的执行偏移地址13指令 iadd 就是将操作栈顶中两个数 200 加上 100, 再把它的和 300 压回栈。

在除非指令(idiv, ldiv)以及求余指令(irem 和 lrem )中, 当出现除数为零时会导致虚拟机抛出 ArithmeticException 异常

3.转换指令

转换指令可以将两种不同的数值类型进行相互转换,一般用于显示类型转换
包含: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l 和 d2f
其中 i 是 int, b 是 byte, c 是 char, s 是 short, l 是long, f 是 float, d 是 double 类型

例子2:
java 源码

public class Test {
    public static void main(String[] args) {
            long zz = 10000;
            int yy = (int)zz;
            int a = 1;
            int b = 2;
            int c = a + b;
        }
}

用 javap 生成的字节码是

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=7, args_size=1
         0: ldc2_w        #2                  // long 10000l , 将常量 1000l 加载到操作栈
         3: lstore_1
         4: lload_1
         5: l2i         // 这里对应 int yy = (int)zz; 进行强转
         6: istore_3
         7: iconst_1
         8: istore        4
        10: iconst_2
        11: istore        5
        13: iload         4
        15: iload         5
        17: iadd
        18: istore        6
        20: return
}

4.对象创建与访问指令

在 Java 中一切皆为对象,类实例和数组都是对象,但是 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。

例3:

public class Test {

    public static void main(String[] args) {
        ByteTest byteTest = new ByteTest();
        byteTest.name = "zhangsan";
        byteTest.age = 13;

        String[] list = new String[1];
        list[0] = "one";
        int size = list.length;
    }

    public static class ByteTest{

        public static String name;

        public int age;
    }
}

生成的部分字节码

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // new 指令,new 一个 ByteTest 对象
         3: dup
         4: invokespecial #3                  // Method com/yxhuang/temp/Test$ByteTest."<init>":()V
         7: astore_1
         8: aload_1
         9: pop
        10: ldc           #4                  // String zhangsan
        12: putstatic     #5                  // 访问 ByteTest 静态字段 name
        15: aload_1
        16: bipush        13
        18: putfield      #6                  // 访问 ByteTest 实例的字段 age
        21: iconst_1
        22: anewarray     #7                  // 创建数组
        25: astore_2
        26: aload_2
        27: iconst_0
        28: ldc           #8                  // String one
        30: aastore
        31: aload_2
        32: arraylength                     // 取数组的长度
        33: istore_3
        34: return

5.操作数栈管理指令

包括

其中 pop 是将栈顶一个元素出栈,pop2 是将栈顶的两个元素出栈;

dup 是复制栈顶数值并将复制值压入栈顶, dup2 是复制栈顶一个(对于 long 或 double 类型)或 两个(非 long, double 类型)的数值弹出;

dup_x1 是复制栈顶的值并将两个复制值压入栈顶,dup_x2 复制栈顶一个(对于 long 或 double 类型)或 两个(非 long, double 类型)的数值并压入栈顶;

dup2_x1 是 dup_x1 的双倍版本, dup2_x2 是 dup_x2 的双倍版本;

java_byte_7.jpg

注:图片来自于JVM 内部原理(六)— Java 字节码基础之一

在 java 中 long 和 double 类型需要占用两个存储空间,为了交换两个 double 值,需要 dup2_x2

java_byte_8.png

6.控制转移指令

控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序

指令 例子 说明
ifeq if (i == 0)
if (bool == false)
当栈顶 int 型数值等于0 或者是 false 跳转
ifne if (i != 0)
if (bool == true)
当栈顶 int 型数值不等于0 或者是 true 跳转
ifge if (i >= 0) 当栈顶 int 型数值大于或等于0时跳转
ifgt if (i > 0) 当栈顶 int 型数值大于0时跳转
ifle if (i <= 0) 当栈顶 int 型数值小于或等于0时跳转
iflt if (i < 0) 当栈顶 int 型数值小于0时跳转
if_icmple 比较栈顶两个 int 型数值的大小,当结果小于或等于0时跳转

在 Java 虚拟机中,各种类型的比较最终都会转化为 int 类型的比较。

例4:

static int gt(int a, int b){
    if (a > b){
        return 1;
    } else {
        return -1;
    }
}

// 生成的字节码
  static int gt(int, int);
    descriptor: (II)I
    flags: ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0                 // 将 a 入栈 local[0]{a}
         1: iload_1                 // 将 b 入栈 local[1]{b}
         2: if_icmple     7         // 如果 local[0] <= local[1] 跳去 7 ,即 iconst_m1
         5: iconst_1     // 将 int 型 1 推送至栈顶
         6: ireturn      // 返回
         7: iconst_m1    // 将 int 型 -1 推送至栈顶
         8: ireturn      // 返回

注意上面的 if_icmple 指令是比较栈顶两个 int 型数值的大小,当结果小于或等于0时跳转, 与我们代码中 a > b 刚好是相反的



例5:

 static int calc(int count){
        int result = 0;
        while (count > 0){
            result += count--;
        }
        return result;
    }
    
// 字节码
 static int calc(int);
    descriptor: (I)I
    flags: ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_0        // count 入栈
         1: istore_1
         2: iload_0
         3: ifle          16   // 当栈顶数值小于或等于0时调到 16, 即跳出 while 选好
         6: iload_1
         7: iload_0
         8: iinc          0, -1   // 自增或自减, 即 count--   
        11: iadd            // result+
        12: istore_1
        13: goto          2 // 回到 2
        16: iload_1
        17: ireturn

例6:

 static int calcSwitch(int type){
        int result = 0;
        switch (type){
            case 1:
                result = 10;
                break;
            case 2:
                result = 11;
                break;
        }
        return result;
    }

// 字节码
static int calcSwitch(int);
    descriptor: (I)I
    flags: ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_0
         3: lookupswitch  { // 2
                       1: 28
                       2: 34
                 default: 37
            }
        28: bipush        10
        30: istore_1
        31: goto          37
        34: bipush        11
        36: istore_1
        37: iload_1
        38: ireturn

7.方法调用和返回指令

8.异常处理指令

arthrow 指令是 Java 程序中显式抛出异常的操作(throw 语句)

9.同步指令

同步一段指令集序列通常是由 Java 语言中的 synchroined 语句来表示, Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronizd.

例7:

  static void sycn(Object object){
       synchronized (object){

       }
   }
   
   // 相应的字节码
   static void sycn(java.lang.Object);
   descriptor: (Ljava/lang/Object;)V
   flags: ACC_STATIC
   Code:
     stack=2, locals=3, args_size=1
        0: aload_0
        1: dup
        2: astore_1
        3: monitorenter
        4: aload_1
        5: monitorexit
        6: goto          14
        9: astore_2
       10: aload_1
       11: monitorexit
       12: aload_2
       13: athrow
       14: return

每条 monitorenter 指令都必须有其对应的 monitorexit 指令,上面例子中的 3 和 5 是对应的。9 是处理异常的,这是编译器会自动产生一个异常处理,同步指令也需要在异常的时候退出,11 monitorexit 就是编译器自动生成在异常时退出的指令。

四、参考

1.【译】如何阅读 Java 字节码

  1. JVM 内部原理(六)— Java 字节码基础之一

  2. 周志明《深入理解 Java 虚拟机》

上一篇下一篇

猜你喜欢

热点阅读