字节码解析

字节码

java 学习过程中,我们编写的基本都是 .java 结尾的文件,这样的文件称为源文件;是不能直接运行的。所以需要将 .java 文件转换为 .class 字节码文件,然后通过JVM虚拟机将 .class 字节码文件,转化为对应系统可运行的机器指令,才能完成运行。

这也就是 java 语言一处编译,处处运行的原理。

(有 JVM 的存在; JVM 能够根据不同的操作系统,将 .class 文件编译成对应系统可支持运行的机器指令。)

1621046472377

字节码的结构

接下来我们会从字节码的结构、执行过程等方面对字节码进行剖析。

源文件

public class Demo {
	
    public static void main(String[] args) {
        int a = 1;
        int b = 1;
        int c = add(a, b);
        System.out.println(c);
    }

    public static int add(int a, int b) {
        return a + b;
    }
}

字节码文件

使用javac命令将源文件编译成字节码文件。

命令:javac -g:vars,lines xxx.javavarslines表示额外编译本地变量表和代码行对照表(后续说明作用)

Demo.class

Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 34 00 1F 0A 00 06 00 11 0A    J~:>...4........
00000010: 00 05 00 12 09 00 13 00 14 0A 00 15 00 16 07 00    ................
00000020: 17 07 00 18 01 00 06 3C 69 6E 69 74 3E 01 00 03    .......<init>...
00000030: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E    ()V...Code...Lin
00000040: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D    eNumberTable...m
00000050: 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61    ain...([Ljava/la
00000060: 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 03 61    ng/String;)V...a
00000070: 64 64 01 00 05 28 49 49 29 49 01 00 0A 53 6F 75    dd...(II)I...Sou
00000080: 72 63 65 46 69 6C 65 01 00 09 44 65 6D 6F 2E 6A    rceFile...Demo.j
00000090: 61 76 61 0C 00 07 00 08 0C 00 0D 00 0E 07 00 19    ava.............
000000a0: 0C 00 1A 00 1B 07 00 1C 0C 00 1D 00 1E 01 00 04    ................
000000b0: 44 65 6D 6F 01 00 10 6A 61 76 61 2F 6C 61 6E 67    Demo...java/lang
000000c0: 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61 2F 6C    /Object...java/l
000000d0: 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74    ang/System...out
000000e0: 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E    ...Ljava/io/Prin
000000f0: 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76 61 2F    tStream;...java/
00000100: 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 00    io/PrintStream..
00000110: 07 70 72 69 6E 74 6C 6E 01 00 04 28 49 29 56 00    .println...(I)V.
00000120: 21 00 05 00 06 00 00 00 00 00 03 00 01 00 07 00    !...............
00000130: 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00    ................
00000140: 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06    .*7..1..........
00000150: 00 01 00 00 00 01 00 09 00 0B 00 0C 00 01 00 09    ................
00000160: 00 00 00 3A 00 02 00 04 00 00 00 12 04 3C 04 3D    ...:.........<.=
00000170: 1B 1C B8 00 02 3E B2 00 03 1D B6 00 04 B1 00 00    ..8..>2...6..1..
00000180: 00 01 00 0A 00 00 00 16 00 05 00 00 00 04 00 02    ................
00000190: 00 05 00 04 00 06 00 0A 00 07 00 11 00 08 00 09    ................
000001a0: 00 0D 00 0E 00 01 00 09 00 00 00 1C 00 02 00 02    ................
000001b0: 00 00 00 04 1A 1B 60 AC 00 00 00 01 00 0A 00 00    ......`,........
000001c0: 00 06 00 01 00 00 00 0B 00 01 00 0F 00 00 00 02    ................
000001d0: 00 10  

开头的四个字符CA FE BA BE表示当前文件是一个字节码文件,JVM会以文件是否以这四个字开头来判断为.class文件,进而进行加载。

注意:.class 文件直接使用记事本打开会乱码,这里推荐使用 vs code 工具,安装 hexdump for VSCode 插件,便能够正常打开字节码文件了。

1621048069820

javap 命令

当我们生成字节码文件后,看得懂吗?是不是一脸懵逼的打开,然后一脸懵逼的关上,心想这什么玩意儿?

不要着急,Java提供了工具命令,能够帮我们更好的观看字节码文件。

javap命令介绍

javapjdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

使用

命令:javap -v Demo.class

以下就是使用javap命令生成好的汇编文件,大致可分为类基本信息、常量池、包含方法

Classfile /C:/Users/Yhf19/Desktop/demo/Demo.class
  Last modified 2021-5-16; size 609 bytes
  MD5 checksum e7474a62c3f9443a0c4b65c7de331a4d
public class Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = Methodref          #5.#25         // Demo.add:(II)I
   #3 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #5 = Class              #30            // Demo
   #6 = Class              #31            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LDemo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               c
  #22 = Utf8               add
  #23 = Utf8               (II)I
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = NameAndType        #22:#23        // add:(II)I
  #26 = Class              #32            // java/lang/System
  #27 = NameAndType        #33:#34        // out:Ljava/io/PrintStream;
  #28 = Class              #35            // java/io/PrintStream
  #29 = NameAndType        #36:#37        // println:(I)V
  #30 = Utf8               Demo
  #31 = Utf8               java/lang/Object
  #32 = Utf8               java/lang/System
  #33 = Utf8               out
  #34 = Utf8               Ljava/io/PrintStream;
  #35 = Utf8               java/io/PrintStream
  #36 = Utf8               println
  #37 = Utf8               (I)V
{
  public Demo();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDemo;

  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: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method add:(II)I
         9: istore_3
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 4
        line 7: 10
        line 8: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            4      14     2     b   I
           10       8     3     c   I

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   I
            0       4     1     b   I
}

类基本信息

Classfile /C:/Users/Yhf19/Desktop/demo/Demo.class
  Last modified 2021-5-16; size 609 bytes
  MD5 checksum e7474a62c3f9443a0c4b65c7de331a4d
public class Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

前三行描述了.class文件的所在路径、最后修改时间、校验和。

后三行描述了类的基本信息。

  • minor:此版本号

  • major:主版本号

  • flags:描述了类的相关修饰符,ACC_PUBLIC表示类是public公开类型,flag可以有以下取值

    1621145552026

常量池

Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = Methodref          #5.#25         // Demo.add:(II)I
   #3 = Fieldref           #26.#27        //java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #5 = Class              #30            // Demo
   #6 = Class              #31            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable

这一部分就是类中使用到的常量,每个常量都有编号,以#号开头,=号后面就是常量类型,类型后就是常量的值。每个常量都由三部分组成——编号、类型、常量值组成。

例如#1常量的类型是Methodref,表示这个常量描述的是方法相关的信息;#5变量的类型是Class,表示这个常量描述的是类相关的信息。

注意:一个常量还可以引用其他常量,例如#2就引用了#5#25常量的值。而最终#2得到的值就是

// Demo.add:(II)I

包含方法

{
  public Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    	// 省略

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      // 省略

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
     // 省略
}

这一部分就是包含方法,也就是在当前类中调用了那些方法,都会在这里一一列举出来。

方法结构

我们以main()方法为例,对方法中的结构进行了解。

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: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method add:(II)I
         9: istore_3
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 4
        line 7: 10
        line 8: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            4      14     2     b   I
           10       8     3     c   I

对于方法我们可以分为方法描述和Code这两部分。

方法描述
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC

方法描述就是对方法签名,权限等一系列的描述。

  • descriptor:精简的描述了方法的入参和出参。

    [:一个"["表示一维数组;

    L:为类描述符,表示Stirng是一个引用类型。

    V: 则表示方法的返回值为void类型,也就是无返回值。

    1621133717542

Code部分

code关键字以下的部分我们也可以进行划分为指令表、本地变量表、代码行对照表三部分。

指令表

stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method add:(II)I
         9: istore_3
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        17: return

该部分第一行是该方法的指令以及执行过程的相关信息,这一行信息包括:

  • args_size 是参数数量,在主函数中,因为有args 这个参数,所以在这里 args_size 为 1;
  • locals 是该方法中的本地变量有多少个,在我们的主函数里面有定义了 3 个变量,加上一个参数,因此有 4 个变量;
  • stack 是方法在执行过程中,操作数栈中最大深度,这个在之后讲解指令执行过程时可以看出。

在这一行信息之后是字节码指令,一条指令包括偏移量以及执行的指令码,PC Register 利用偏移量来判断指令执行位置。

代码行对照表

LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 4
        line 7: 10
        line 8: 17

line 4: 0为例,表示源.java文件的第四行代码,对应的是0偏移量

1621149295268

本地变量表

LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            4      14     2     b   I
           10       8     3     c   I
  • start:为这个变量可见的起始偏移位置,它的值必须是在 Code 中存在的偏移量值。(也就是这个变量第一次出现在哪个偏移量)

  • length:为该变量的有效长度,描述的是该变量在方法中的有效范围大小。

    在这个例子中,我们的变量直到方法末尾都有效,因此你会发现 start + lenth 的值都是 18 (方法中执行的指令数)。当我们在一个局部的代码块里面声明一个变量,那么它的有效期长度将会更短。

  • slot:为变量在 local variable(本地变量表) 中的位置,这可以帮助我们在指令中确定对应的变量。

  • Name 则是变量名

  • Signature 为该变量的类型

当我们了解了class文件的结构之后,我们就可以进行流程分析,但是在分析之前,我们还需要了解以下JVM的内存模型,能够更好的帮助我们进行分析。

JVM内存模型

我们都知道在Java存在多线程,当多个线程访问同一个资源的时候,我们就称这个资源为共享资源;当然也有每个线程独有的资源。

我们就根据资源的可用范围将JVM进行以下划分。

JVM内存模型

1621150300590

其中方法区和堆内存是资源共享的,所有线程都可以进行访问。

虚拟机栈、本地方法栈以及程序计数器是线程独有的;

  • PC Register(程序计数器) 用于记录当前线程指令的执行位置。由于一个进程可能有多个线程,而 CPU 会在不同线程之间切换,为了能够记录各个线程的当前执行的指令,每个线程都需要有一个 PC Register,来保证各个线程都可以进行独立运算。
  • JVM Stack(虚拟机栈) 用于存放调用方法时压入栈的栈帧。相信学过数据结构的对栈应该不陌生,JVM Stack 压入的单位为栈帧(Frame),用于存储数据、动态链接、方法返回值和调度异常等。每次调用一个方法都会创建一个新的栈帧压入 JVM Stack 来存储该方法的信息,当该方法调用完成时,对应的栈帧也会跟着被销毁。一个栈帧都有自己的局部变量数组操作数栈对当前方法类的运行常量池的引用
  • Native Method Stack(本地方法栈) 则是用于调用操作系统本地方法时使用的栈空间。

当我们调用一个方法的时候,就会创建一个新的栈帧,然后继续入栈(JVM stack),执行完成后栈帧销毁(也成为弹栈)。所以在栈帧中包含了哪些东西,才能够支撑栈帧的执行呢,我们来看看。

栈帧与JVM stack

1621151437080

每个栈帧中都存放了本地变量表、操作栈以及常量引用池

指令的执行过程

基本指令含义

接下来我们一步步执行方法中的指令,在这里我们先对出现的几个指令做一个简单的介绍:

  • iconst_<i> 放一个 int 常量放到 operand stack 中
  • istore_<n> 从 operand stack 中获取一个 int 到 local variable 的 n 中
  • iload_<n> 从 local variable 中读取 int 变量 n 的值到操作数栈中
  • invokestatic 调用一个 class 的 static 方法
  • getstatic 从 class 中获取一个 static 字段
  • invokevirtual 调用一个实例方法,基于类的调度
  • return 从方法中返回一个 void,ireturn 从方法中返回 operand stack 栈顶的 int

更多的指令与详细的说明请查看文章最后参考中的官方指令文档

这里要注意,操作数栈的数是被取出操作,被取出的数将不会继续在 operand stack 里面。

main()方法指令执行分析

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
			// 把int常量1放入操作栈
         0: iconst_1
         	// 从操作栈中取出1常量,放入本地变量表Slot为1位置
         1: istore_1
            // 把int常量1放入操作栈
         2: iconst_1
         	// 从操作栈中取出1常量,放入本地变量表Slot为2位置
         3: istore_2
         	// 将常量1赋值给本地变量表Slot为1位置的变量a
         4: iload_1
         	// // 将常量1赋值给本地变量表Slot为2位置的变量b
         5: iload_2
         	// 调用静态方法add(),并传入(II)两个Int类型的参数,返回值为I int类型
         6: invokestatic  #2                  // Method add:(II)I
         	// 将add()方法返回的数据由操作栈,存储到本地变量表Slot为3位置
         9: istore_3
         	// 获取静态字段
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        	// 将本地变量表Solt为3位置上的常量3赋值给对应位置变量c
        13: iload_3
        	// 调用println方法,传入int变量,该方法返回值为Void
        14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        	// main方法返回
        17: return
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 4
        line 7: 10
        line 8: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            4      14     2     b   I
           10       8     3     c   I

总结:

  1. 在方法作为栈帧入栈的时候,本地变量表就已经生成了,每个Slot对应了哪个变量也已经就绪。
  2. 凡是生成的常量生成后都需由 operand stack 存储,再由指令store存储到 local variable Table中。

美团文章参考:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

InfoQ文章参考:https://xie.infoq.cn/article/a9fbc16488d3ebbe4b758ff92

指令参考:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html

入了皮毛,后续理解深入后在做修改。

当作初学者。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值