Java字节码学习笔记(一):Java字节码是什么?

导读:本篇文章讲解 Java字节码学习笔记(一):Java字节码是什么?,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、字节码来源

    我们都知道 Java 有 JVM 的设计是可以跨平台运行的,它在诞生之期提出过一个著名口号:

一次编写,到处运行(write once,run anywhere)

    平台无关性的理想最终在操作系统的运用层上:虚拟机提供商开发了许多可以运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了“ 一次编写,到处运行”
    字节码是由十六进制组成的,JVM 以两个十六进制为一组,即以字节为单位进行读取。在 Java 中使用 javac 命令把源代码编译成字节文件,一个 .java 源文件从编译成 .class 字节码文件,示意图如下:

Java字节码编译
    通过字节码可以更准确、更直观地理解 Java 更深层次的东西,通过字节码,我们可以很直观的看到 volatile 关键字如何在字节码上生效。另外,字节码增强技术在各种 ORM 框架、Spirng AOP、热部署等一些应用中经常使用,深入理解其原理对于我们大有裨益。

2、字节码结构

2.1、生成字节码

    Java 源文件通过 javac 命令编译之后就会得到 .class 结尾的字节码文件,下面是测试代码:

package com.leo.test;
public class Main {
    private int numberA =1 ;
    public static void main(String[] args) {
        Main test = new Main();
        int num = 1;
        Object obj = "2";
        long count = 3L;
        boolean flag = false;
        short[] arr = {4, 5, 6};
        test.load(num, obj, count, flag, arr);
    }
    // 1 局部变量入栈命令
    public void load(int num, Object obj, long count, boolean flag, short[] arr) {
        System.out.println(num);
        System.out.println(obj);
        System.out.println(count);
        System.out.println(flag);
        System.out.println(arr);
    }
}

编译后生成的 .class 字节码文件,打开之后是一堆十六进制数,如下:

cafe babe 0000 0034 0040 0a00 0e00 2a09
0003 002b 0700 2c0a 0003 002a 0800 2d05
0000 0000 0000 0003 0a00 0300 2e09 002f
0030 0a00 3100 320a 0031 0033 0a00 3100
340a 0031 0035 0700 3601 0007 6e75 6d62
6572 4101 0001 4901 0006 3c69 6e69 743e
0100 0328 2956 0100 0443 6f64 6501 000f
4c69 6e65 4e75 6d62 6572 5461 626c 6501
0012 4c6f 6361 6c56 6172 6961 626c 6554
6162 6c65 0100 0474 6869 7301 0013 4c63
6f6d 2f6c 656f 2f74 6573 742f 4d61 696e
3b01 0004 6d61 696e 0100 1628 5b4c 6a61
7661 2f6c 616e 672f 5374 7269 6e67 3b29
5601 0004 6172 6773 0100 135b 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0474 6573 7401 0003 6e75 6d01 0003 6f62
6a01 0012 4c6a 6176 612f 6c61 6e67 2f4f
626a 6563 743b 0100 0563 6f75 6e74 0100
014a 0100 0466 6c61 6701 0001 5a01 0003
6172 7201 0002 5b53 0100 046c 6f61 6401
001a 2849 4c6a 6176 612f 6c61 6e67 2f4f
626a 6563 743b 4a5a 5b53 2956 0100 0a53
6f75 7263 6546 696c 6501 0009 4d61 696e
2e6a 6176 610c 0011 0012 0c00 0f00 1001
0011 636f 6d2f 6c65 6f2f 7465 7374 2f4d
6169 6e01 0001 320c 0026 0027 0700 370c
0038 0039 0700 3a0c 003b 003c 0c00 3b00
3d0c 003b 003e 0c00 3b00 3f01 0010 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 0100
106a 6176 612f 6c61 6e67 2f53 7973 7465
6d01 0003 6f75 7401 0015 4c6a 6176 612f
696f 2f50 7269 6e74 5374 7265 616d 3b01
0013 6a61 7661 2f69 6f2f 5072 696e 7453
7472 6561 6d01 0007 7072 696e 746c 6e01
0004 2849 2956 0100 1528 4c6a 6176 612f
6c61 6e67 2f4f 626a 6563 743b 2956 0100
0428 4a29 5601 0004 285a 2956 0021 0003
000e 0000 0001 0002 000f 0010 0000 0003
0001 0011 0012 0001 0013 0000 0038 0002
0001 0000 000a 2ab7 0001 2a04 b500 02b1
0000 0002 0014 0000 000a 0002 0000 0002
0004 0003 0015 0000 000c 0001 0000 000a
0016 0017 0000 0009 0018 0019 0001 0013
0000 00b6 0007 0008 0000 0034 bb00 0359
b700 044c 043d 1205 4e14 0006 3704 0336
0606 bc09 5903 0756 5904 0856 5905 1006
563a 072b 1c2d 1604 1506 1907 b600 08b1
0000 0002 0014 0000 0022 0008 0000 0005
0008 0006 000a 0007 000d 0008 0012 0009
0015 000a 0027 000b 0033 000c 0015 0000
0048 0007 0000 0034 001a 001b 0000 0008
002c 001c 0017 0001 000a 002a 001d 0010
0002 000d 0027 001e 001f 0003 0012 0022
0020 0021 0004 0015 001f 0022 0023 0006
0027 000d 0024 0025 0007 0001 0026 0027
0001 0013 0000 0096 0003 0007 0000 0026
b200 091b b600 0ab2 0009 2cb6 000b b200
0921 b600 0cb2 0009 1505 b600 0db2 0009
1906 b600 0bb1 0000 0002 0014 0000 001a
0006 0000 000f 0007 0010 000e 0011 0015
0012 001d 0013 0025 0014 0015 0000 003e
0006 0000 0026 0016 0017 0000 0000 0026
001d 0010 0001 0000 0026 001e 001f 0002
0000 0026 0020 0021 0003 0000 0026 0022
0023 0005 0000 0026 0024 0025 0006 0001
0028 0000 0002 0029 

2.2、字节码结构

    JVM 对于字节码规范是有要求的,上述字节码看似混乱无章其实是按照 JVM 规范要求每一个字节码文件都要由十部分固定的顺序组成的,组成结构图如下:
字节码结构图

2.2.1、魔数(Magic Number

    每个字节码文件的头 4 个字节称为魔数(Magic Number),它的作用就是检验这个文件是否是正常的 .class 文件。很多地方都有魔数,比如 gif 或者 jpg 等文件头中。魔数的固定值为:0xCAFEBABE。

魔数的固定值是 Java 之父 James Gosling 制定的,为 CafeBabe(咖啡宝贝),而 Java 的图标为一杯咖啡。

2.2.2、版本号(Version)

    版本号为魔数之后的 4 个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version),比如我的就是:

0000 0034

次版本为0,主版本号转换成十进制为 52(0034 十六进制转十进制),在 Oracle 52 对应的 JDK版本为 1.8。
jdk版本

2.2.3、常量池(Constant Pool)

    紧接着主版本号之后的字节是常量池入口。常量池中存储两种类型常量:字面量符号引用
字面量:为代码中声明为 final 的常量值
符号引用:如类和接口的全局限定名、字段名称和描述符、方法名称和描述符。
常量池整体上分为两部分:常量池计数器常量池数据区
常量池结构
常量池计数器(constant_pool_count):由常量池的数量不固定,所以需要先放两个字节来表示常量池容量计数值,比如我这里是 0040 ,换算成十进制为 64 ,排除下标 0,也及时说这个类文件有 63 个常量。
常量池数据区:数据区是由(constant_pool_count -1)个 cp_info 结构组成,一个 cp_info 的结构对应一个常量。在字节码中共有 14 种类型的 cp_info,每种类型的机构都是固定的。
常量数据池类型
cp_info 结构在本文不展开说,都是先通过 tag 来标识类型,然后后续的 n 个字节来描述长度和数据
具体的结构图

2.2.4、访问标志(access_flag)

    常量池结束之后的两个字节,描述该 Class 是类还是接口,已经是否被 publicabstractfinal 等修饰符修饰。JVM 并没有穷举所有的访问表示,而是使用 按位或 操作来进行描述的,比如某个类的修饰符位 public final,则对应的访问修饰符的值为 ACC_PUBLIC | ACC_FINAL,即 0x0001 | 0x0010 = 0x0011

标志名称 标志值 含义
ACC_PUBLIC 0x0001 public 类型
ACC_PRIVATE 0x0002 private 类型
ACC_PROTECTED 0x0004 protected 类型
ACC_INTERFACE 0x0200 接口修饰符
ACC_STATIC 0x0008 字段是否为 static
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义
ACC_FINAL 0x0100 字段是否为 final
ACC_VOLATILE 0x0040 字段是否为 volatile
ACC_TRANSIENT 0x0080 字段是否为 transient
ACC_ABSTRACT 0x0400 abstract 修饰符
ACC_ANNOTATION 0x2000 注解修饰符
ACC_SYNCHETIC 0x1000 字段是否为 编译器自动产生
ACC_ENUM 0x4000 字段是否为 enum

2.2.5、当前类名(this_class)

    访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

2.2.6、父亲类名(super_class)

    当前类名的后两个字节,描述父类的全限定名。这两个字节保存的值也是在常量池中的索引值,根据索引值就能在常量池中找到这个类的父类的全限定名。

2.2.7、接口信息(interfaces)

    父类名称后的两个字节,描述这个类的接口计数器,即: 当前类或父类实现的接口数量。紧接着的 n 个字节是所有的接口名称的字符串常量在常量池的索引值。

2.2.8、字段表(field_table)

    字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的 局部变量。

字段表也分为两部分:
第一部分:两个字节,描述字段个数;
第二部分:每个字段的详细信息 field_info
字段表结构
根据 demo 中声明的变量:private int numberA 来解释上述信息:
描述详细信息

2.2.9、方法表(method_table)

    字段表结束后为方发表,方法表也是由两个部分组成。
第一部分:两个字节描述方法的个数
第二部分:每个方法的详细信息。

方法的详细信息包括:方法的访问标志、方法名、方法的描述符、方法的属性。
方法的详细信息
方法名和方法的描述都是常量池的索引值,可以通过索引值在常量池中查询得到。方法属性略微复杂,我们通过 javap -verbose 反编译,可以看到属性中包含三个部分:

  • Code 区:源代码对应的 JVM 指令操作码,我们在字节码增强的时候重点操作的就是这个部分。
  • LineNunberTable:行号表,将 Code 区的操作码和源代码的行号对应,Debuge 时会起到作用(即当源代码乡下走一行,相应的需要走几个 JVM 指令操作码)。
  • LoalVariableTable:本地变量表,包含 this 和局部变量,之所以可以在每一个非 static 的方法内部都可以调用到 this,是应为 JVM 将 this 作为每一个方法的第一个参数隐式进行传入。
  public void load(int, java.lang.Object, long, boolean, short[]);
    descriptor: (ILjava/lang/Object;JZ[S)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=7, args_size=6
         0: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iload_1
         4: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
         7: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        14: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: lload_3
        18: invokevirtual #12                 // Method java/io/PrintStream.println:(J)V
        21: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload         5
        26: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
        29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        32: aload         6
        34: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        37: return
      LineNumberTable:
        line 15: 0
        line 16: 7
        line 17: 14
        line 18: 21
        line 19: 29
        line 20: 37
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      38     0  this   Lcom/leo/test/Main;
            0      38     1   num   I
            0      38     2   obj   Ljava/lang/Object;
            0      38     3 count   J
            0      38     5  flag   Z
            0      38     6   arr   [S
}
SourceFile: "Main.java"

2.2.10、附加属性表(addition_attribute_table)

    字节码的最后一部分,存放了在文件中类或接口所定义的属性的基本信息。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72603.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!