jvm

如何揭开字节码的神秘面纱?

基于案例进行解读字节码

Posted by MasterJen on November 24, 2018

Hey Bytecode

I am a slow walker,but I never walk backwards.– 我走得很慢,但是我从来不会后退.

在今天的Blog开始之前首先给大家演示个案例. 爱码如下;

public class TestThrowException {
    public static void main(String[] args) {
        int result = testJvm();
        System.out.println(result);
    }

    private static int testJvm() {
        int a = 10;
        try {
            a = 20;
            throw new RuntimeException();
        } catch (Exception e) {
            a = 30;
            return a;
        } finally {
            a = 40;
            System.out.println(a);
        }
    }
}

如果此时执行main方法,那么result究竟是多少呢?有的人也许认为是40 ,也有的人认为是30 ,那么究竟是多少呢?

运行结果如下:

40
30

a 确实明明输出的是40 ,但是为什么返回的是 30 呢? 这时候我们就开始 Debug了,但是突然发现,Debug 中 a 也是变成 40 了,这时候,就有点束手无策了.

如果想要了解真正的实现过程,那么我们必须了解一个名词,就是字节码.

官方语言:

字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象.它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型.字节码被这样叫是因为通常每个 opcode 是一字节长,但是指令码的长度是变化的.每个指令有从 0 到 255(或十六进制的: 00 到FF)的一字节操作码,被参数例如寄存器或内存地址跟随.

好吧,比较笼统,那么就直接演示吧.

首先找到我们的测试类的绝对路径 然后通过编译得到class文件,

javac TestThrowException.java

进行反编译得到字节码文件 并起个别名

 javap -p -private  -verbose TestThrowException > TestThrow.bytecode

然后打开字节码文件 会发现一大串的 信息. 不要着急,我们慢慢来.首先我们先对JVM进行了解下.

 Last modified 2018-11-23; size 727 bytes
  MD5 checksum d453ea76fd94d0a9e335c83e338d8a79
  Compiled from "TestThrowException.java"
public class com.master.ggt.jvm.TestThrowException
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
   #2 = Methodref          #8.#24         // com/master/ggt/jvm/TestThrowException.testJvm:()I
   #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   #5 = Class              #29            // java/lang/RuntimeException
   #6 = Methodref          #5.#23         // java/lang/RuntimeException."<init>":()V
   #7 = Class              #30            // java/lang/Exception
   #8 = Class              #31            // com/master/ggt/jvm/TestThrowException
   #9 = Class              #32            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               testJvm
  #17 = Utf8               ()I
  #18 = Utf8               StackMapTable
  #19 = Class              #30            // java/lang/Exception
  #20 = Class              #33            // java/lang/Throwable
  #21 = Utf8               SourceFile
  #22 = Utf8               TestThrowException.java
  #23 = NameAndType        #10:#11        // "<init>":()V
  #24 = NameAndType        #16:#17        // testJvm:()I
  #25 = Class              #34            // java/lang/System
  #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
  #27 = Class              #37            // java/io/PrintStream
  #28 = NameAndType        #38:#39        // println:(I)V
  #29 = Utf8               java/lang/RuntimeException
  #30 = Utf8               java/lang/Exception
  #31 = Utf8               com/master/ggt/jvm/TestThrowException
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/Throwable
  #34 = Utf8               java/lang/System
  #35 = Utf8               out
  #36 = Utf8               Ljava/io/PrintStream;
  #37 = Utf8               java/io/PrintStream
  #38 = Utf8               println
  #39 = Utf8               (I)V
{
  public com.master.ggt.jvm.TestThrowException();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 18: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokestatic  #2                  // Method testJvm:()I
         3: istore_1
         4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: iload_1
         8: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        11: return
      LineNumberTable:
        line 20: 0
        line 21: 4
        line 22: 11

  private static int testJvm();
    descriptor: ()I
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=0
         0: bipush        10
         2: istore_0
         3: bipush        20
         5: istore_0
         6: new           #5                  // class java/lang/RuntimeException
         9: dup
        10: invokespecial #6                  // Method java/lang/RuntimeException."<init>":()V
        13: athrow
        14: astore_1
        15: bipush        30
        17: istore_0
        18: iload_0
        19: istore_2
        20: bipush        40
        22: istore_0
        23: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_0
        27: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        30: iload_2
        31: ireturn
        32: astore_3
        33: bipush        40
        35: istore_0
        36: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: iload_0
        40: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        43: aload_3
        44: athrow
      Exception table:
         from    to  target type
             3    14    14   Class java/lang/Exception
             3    20    32   any
      LineNumberTable:
        line 25: 0
        line 27: 3
        line 28: 6
        line 30: 14
        line 31: 15
        line 32: 18
        line 34: 20
        line 35: 23
        line 32: 30
        line 34: 32
        line 35: 36
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 14
          locals = [ int ]
          stack = [ class java/lang/Exception ]
        frame_type = 81 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}

JVM java Virtual Machine

具有完整的硬件系统功能,并运行在完全隔离的环境中的计算机系统,现在市面上比较实用的是 HotSpot 虚拟机.

浅聊JVM内存结构

  • 虚拟机栈

    • 线程私有空间,JVM栈的生命周期与线程相同,用于存储栈帧.

    • 栈帧是JVM描述java方法执行的内存模型,存储了方法的局部变量,中间演算结果,方法的返回结果等信息.

    • 每一个方法从调用开始直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程.

  • 本地方法栈

    • 发挥作用与虚拟机栈相似,区别是虚拟机栈为虚拟机执行字节码服务,本地方法栈为虚拟机本地使用到的Native 方法服务.

    • HotSpot虚拟机 直接把本地方法栈与虚拟栈合二为一了.

  • java堆

    • 堆是所有的线程共享的数据区域,是JVM中内存最大的一块区域

    • 因为是 GC垃圾回收的主要点,因此也称为 “GC 堆”.

  • 程序计数器

    • 存储指令的地址
  • 方法区

    • 在JVM 程序启动时 执行 用于存储已经被JVM加载的类信息,常量,静态常量等元数据.

    • 物理位置并没有脱离堆,但是逻辑位置趋势独立的 ,所以又称为 非堆.或者永久代 .后来又称为 元空间(Metaspace).

大家也许基本了解到了JVM的内存结构,那么虚拟机栈里的栈帧结构又是什么呢?

栈帧

  • 执行机制

    • 一个方法就是一个栈帧结构

    • 一个方法从调用执行到执行完毕的过程,就对应一个栈帧在JVM栈中从入栈到出栈的过程.

  • 栈帧结构

    • 栈帧是JVM描述方法执行的内存模型,每个方法在执行的同时,都会创建一个栈帧,其中包括了局部变量表,操作数栈,动态链接,方法返回地址,附加信息等.
  • 局部变量表

    • 一组变量值的存储空间,用于存储方法在执行过程中的所需的参数和局部变量.

    • 局部变量表中的每个存储空间又称为 Slot 变量槽 可存储的类型有 byte,short,int,char,float,boolean,reference,returnAdress.其中reference 就是对象地址,returnAddress其实也是基本数据类型,就是说 应该有九种基本类型数据,只是,returnAddress没有字面值类型数据,但是有三个JVM指令,即jsr, ret以及jsr_w ,不过在 jdk1.43之后,就用了 Exception Table 代替了这三个指令.因此常用的就是 8种了.

    • 对于 double 和 long 来说 ,就是连续的两个 Slot 槽,因为每个 Slot 默认的占32 位,就是4个字节,两个就是8个字节.

  • 操作数栈

    • 就是操作栈,是虚拟机的工作区.

    • 通过字节码指令对数据进行操作.

  • 动态连接

    • 每个栈帧内部都包含一个指向运行时常量池中的该栈帧所属方法的引用.

    • 在每一次执行期间 动态的将符号引用换成直接引用.(入口地址)

    • 静态解析是指 类加载时或者首次调用期间将 #1 methodref xxx()V 转换成该方法的入口地址,就是直接引用.

    • 动态连接是每次调用期间将#2 methodref xxx()V 转换成该方法的入口地址,就是直接引用.

  • 方法返回地址

    • 方法的结束分为正常结束和异常结束.
  • JVM 异常

    • StackOverflowError

      • 无穷递归 导致栈溢出

      • 空间不足,无法扩充时,抛出异常.

    • OutOfMemoryError

      • 无线创建线程对象.

      • 所有的可用内存空间,全部分配给JVM去创建线程,导致计算机假死.

了解完这些结构以及作用后,那么我们就可以对上述中的案例 的字节码进行解读了,如下:

Classfile ...
  // 文件的加密 方式
  MD5 checksum d453ea76fd94d0a9e335c83e338d8a79
  // 编译来源
  Compiled from "TestThrowException.java"
public class com.master.ggt.jvm.TestThrowException
// 版本号  从 45 开始
  minor version: 0
  // 到52  正好 7个 对应 jdk 的版本
  major version: 52
  // 超类  就是 该类
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//  动态连接 符号引用 当使用时 直接转换成了 入口地址  初始化
   #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
   // 调用 testJvm时
   #2 = Methodref          #8.#24         // com/master/ggt/jvm/TestThrowException.testJvm:()I
   //符号引用  调用 PrintStream时
   #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   // println:(I)V
   #4 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   //运行异常类 RuntimeException
   #5 = Class              #29            // java/lang/RuntimeException
   #6 = Methodref          #5.#23         // java/lang/RuntimeException."<init>":()V
   #7 = Class              #30            // java/lang/Exception
   #8 = Class              #31            // com/master/ggt/jvm/TestThrowException
   #9 = Class              #32            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               testJvm
  #17 = Utf8               ()I
  #18 = Utf8               StackMapTable
  #19 = Class              #30            // java/lang/Exception
  #20 = Class              #33            // java/lang/Throwable
  #21 = Utf8               SourceFile
  #22 = Utf8               TestThrowException.java
  #23 = NameAndType        #10:#11        // "<init>":()V
  #24 = NameAndType        #16:#17        // testJvm:()I
  #25 = Class              #34            // java/lang/System
  #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
  #27 = Class              #37            // java/io/PrintStream
  #28 = NameAndType        #38:#39        // println:(I)V
  #29 = Utf8               java/lang/RuntimeException
  #30 = Utf8               java/lang/Exception
  #31 = Utf8               com/master/ggt/jvm/TestThrowException
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/Throwable
  #34 = Utf8               java/lang/System
  #35 = Utf8               out
  #36 = Utf8               Ljava/io/PrintStream;
  #37 = Utf8               java/io/PrintStream
  #38 = Utf8               println
  #39 = Utf8               (I)V
{
  public com.master.ggt.jvm.TestThrowException();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    //  默认的构造方法
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 18: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
     //  main 方法里面的  stack 栈的深度2 一个是 得到testJVM() 值,一个是输出 locals 两个变量 一个是 result一个是 System.out 的out对象
      stack=2, locals=2, args_size=1
      //  用以调用类方法(Invoke a class (static) method )
         0: invokestatic  #2                  // Method testJvm:()I
         // 存储  int型
         3: istore_1
         // 得到 输出对象
         4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         //  进行加载 得到 输出 int类型 的 1位置值
         7: iload_1
         // 指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class)
         8: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        11: return
      LineNumberTable:
        line 20: 0
        line 21: 4
        line 22: 11

  private static int testJvm();
    descriptor: ()I
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
    // 最大深度2个 4个变量值 其中两个 栈的深度是 a对象 和 out对象
      stack=2, locals=4, args_size=0
      // 进行 把一个 int型的值 压栈
         0: bipush        10
         // 存储
         2: istore_0
         3: bipush        20
         5: istore_0
         6: new           #5                  // class java/lang/RuntimeException
         9: dup
        10: invokespecial #6                  // Method java/lang/RuntimeException."<init>":()V
        13: athrow
        14: astore_1
        15: bipush        30
        17: istore_0
        18: iload_0
        19: istore_2
        20: bipush        40
        22: istore_0
        23: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_0
        27: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        30: iload_2
        31: ireturn
        32: astore_3
        33: bipush        40
        35: istore_0
        36: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: iload_0
        40: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        43: aload_3
        44: athrow
        // 异常表  代替了 基本类型returnAddress 的三个指令
      Exception table:
      // 当 没有出现异常时  执行 3- 14
         from    to  target type
             3    14    14   Class java/lang/Exception
             //  当出现了任何类型的错误异常时  执行  32默认的执行程序
             3    20    32   any
      LineNumberTable:
        line 25: 0
        line 27: 3
        line 28: 6
        line 30: 14
        line 31: 15
        line 32: 18
        line 34: 20
        line 35: 23
        line 32: 30
        line 34: 32
        line 35: 36
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 14
          locals = [ int ]
          stack = [ class java/lang/Exception ]
        frame_type = 81 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}
SourceFile: "TestThrowException.java"

基本的字节码指令就完成了,有时间我会详细整理一份详细的关于jvm 的信息,然后分享给大家,共同学习.