字节码介绍

什么是字节码

Java低字节校验和算法 java 字节码_java

Java的源代码中编译后会生成一个class文件,文件内容为一些JAVA虚拟机指令,这些指令的内容,由多个十六进制值组成,两个十六进制值为一组,例如:

Java虚拟机的指令由一个字节长度、代表某种特定操作含义的操作码(opcode)以及跟随其后的零个或多个的操作数(operand)构成。

Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制。二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。

字节码的应用

对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种O RM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

解读字节码

编写如下代码

Java低字节校验和算法 java 字节码_java_02

直接解读

intTests.java的源代码编译后,可以通过notepad++(需要安装一下HEX-Editor插件)打开IntTests.class文件,文件内容默认是一种16进制的格式,例如:

Java低字节校验和算法 java 字节码_java_03

Javap指令应用

在IntTests.class目录使用如下代码对类进行反编译,例如:

javap -verbose IntTests.class  //可以使用javap –help查看帮助
Classfile /E:/TCGBIV/DEVCODES/JAVACODES/01-java/target/classes/com/java/jvm/bytecode/IntT
ests.class
 Last modified 2022-4-30; size 604 bytes
 MD5 checksum fa079575306a9cf10a1a61cfc4722e88
 Compiled from "IntTests.java"
public class com.java.jvm.bytecode.IntTests
 minor version: 0
 major version: 52
 flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  \#1 = Methodref      #5.#23     // java/lang/Object."<init>":()V
  \#2 = Fieldref      #24.#25     // java/lang/System.out:Ljava/io/PrintStream;
  \#3 = Methodref      #26.#27     // java/io/PrintStream.println:(I)V
  \#4 = Class        #28       // com/java/jvm/bytecode/IntTests
  \#5 = Class        #29       // java/lang/Object
  \#6 = Utf8        <init>
  \#7 = Utf8        ()V
  \#8 = Utf8        Code
  \#9 = Utf8        LineNumberTable
 \#10 = Utf8        LocalVariableTable
 \#11 = Utf8        this
 \#12 = Utf8        Lcom/java/jvm/bytecode/IntTests;
 \#13 = Utf8        main
 \#14 = Utf8        ([Ljava/lang/String;)V
 \#15 = Utf8        args
 \#16 = Utf8        [Ljava/lang/String;
 \#17 = Utf8        a
 \#18 = Utf8        I
 \#19 = Utf8        b
 \#20 = Utf8        c
 \#21 = Utf8        SourceFile
 \#22 = Utf8        IntTests.java
 \#23 = NameAndType     #6:#7      // "<init>":()V
 \#24 = Class        #30       // java/lang/System
 \#25 = NameAndType     #31:#32    // out:Ljava/io/PrintStream;
 \#26 = Class        #33       // java/io/PrintStream
 \#27 = NameAndType     #34:#35     // println:(I)V
 \#28 = Utf8        com/java/jvm/bytecode/IntTests
 \#29 = Utf8        java/lang/Object
 \#30 = Utf8        java/lang/System
 \#31 = Utf8        out
 \#32 = Utf8        Ljava/io/PrintStream;
 \#33 = Utf8        java/io/PrintStream
 \#34 = Utf8        println
 \#35 = Utf8        (I)V
{
 public com.java.jvm.bytecode.IntTests();
  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 3: 0
   LocalVariableTable:
    Start  Length  Slot  Name  Signature
      0    5   0  this  Lcom/java/jvm/bytecode/IntTests;
 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
     2: istore_1
     3: bipush     20
     5: istore_2
     6: iload_1
     7: iload_2
     8: iadd
     9: istore_3
    10: getstatic   #2          // Field java/lang/System.out:Ljava/io/PrintSt
ream;
    13: iload_3
    14: invokevirtual #3          // Method java/io/PrintStream.println:(I)V
    17: return
   LineNumberTable:
    line 5: 0
    line 6: 3
    line 7: 6
    line 8: 10
    line 9: 17
   LocalVariableTable:
    Start  Length  Slot  Name  Signature
      0    18   0  args  [Ljava/lang/String;
      3    15   1   a  I
      6    12   2   b  I
      10    8   3   c  I
}
SourceFile: "IntTests.java"

jclasslib插件应用

如果每次查看反编译后的字节码都使用javap命令的话,会非常繁琐。这里推荐一个Idea插件,这个插件的名字为jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

Java低字节校验和算法 java 字节码_jvm_04

字节码结构分析

整体结构

一个class类文件的结构组成如下(u代表一个字节无符号int,其余info类型是复合结构):

ClassFile {

  u4       magic;

  u2       minor_version;

  u2       major_version;

  u2       constant_pool_count;

  cp_info     constant_pool[constant_pool_count-1];

  u2       access_flags;

  u2       this_class;

  u2       super_class;

  u2       interfaces_count;

  u2       interfaces[interfaces_count];

  u2       fields_count;

  field_info   fields[fields_count];

  u2       methods_count;

  method_info   methods[methods_count];

  u2        attributes_count;

  attribute_info attributes[attributes_count];

}

其中:

 magic(魔数)

 minor_version(次版本号)

 major_version(主版本号)

 constant_pool_count(常量池计数器)

 constant_pool[constant_pool_count-1](常量池)

 access_flags(类的访问标志)

 this_class(当前类名索引值)

 super_class(父类名索引值)

 interfaces_count(接口计数)

 interfaces[interfaces_count](接口数组)

 fields_count(成员变量计数)

 fields[fields_count](成员变量数组)
 
 methods_count(方法计数)

 methods[methods_count](方法数组)

 attributes_count(属性计数)

 attributes[attributes_count](属性数组)
魔数

所有.class文件的前四个字节都是魔数(Magic Number),是class文件的标识。

魔数的固定值为:0xCAFEBABE。

魔数放在文件开头,JVM可以根据文件魔数判断.class文件的合法性。

版本号

版本号为魔数之后的4个字节。例如,版本号“00 00 00 34”。

前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。例如,版本号“00 00 00 34”的次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

版本号和JAVA编译器的对应关系如图所示:

Java低字节校验和算法 java 字节码_jvm_05

说明,不同版本的Java编译器编译的class文件版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器编译生成的class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的class文件。否则JVM可能会抛出如下异常:

java.lang.UnsupportedClassVersionError
常量池

常量池(Constant Pool)整体上分为两部分,常量池计数器以及常量池数据区。

常量池计数器

ü 版本号后面是常量池数量,用两个字节表示。如图所示:

Java低字节校验和算法 java 字节码_jvm_06

ü 常量池容量计数值从1开始,表示常量池有多少个常量,假如constant_pool_count=1表示有0个常量。这里的0024的值为36,其实常量数为35个。
ü 索引值0用于表达不引用任何常量池。

常量池数据区

ü 数据区是由constant_pool_count-1个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info,每种类型的结构都是固定的。

ü 常量池中存储两类常量:字面量(Literal)与符号引用(Symbolic References)。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。

ü 常量池中的每个常量都是一个表,用于描述不同结构数据,例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jQrDQbhG-1656579984422)(file:///C:\Users\彭\AppData\Local\Temp\ksohtml3188\wps9.jpg)]

其中,cp_info整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose 类名命令,查看JVM反编译后的完整常量池,就可以看到反编译结果,将每一个cp_info结构的类型和值都很明确地呈现了出来。

访问标识

常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。

JVM规范规定了多个访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

类型引用

在访问标记后,会指定该类的类别、父类类别以及实现的接口。

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

父类索引:当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

接口索引:父类名称后的两字节,描述了该类或父类实现了哪些接口,接口的数量以及所有接口名称的字符串常量的索引值。

字段表集合

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

字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_in。

方法表集合

字段表结束后为方法表,用于描述每个方法的信息。

方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性。

属性表集合

字段表集合

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

字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_in。

方法表集合

字段表结束后为方法表,用于描述每个方法的信息。

方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性。

属性表集合

方法表集合之后是属性表集合,用于描述的是class文件所携带的辅助信息,比如class文件对应的源文件信息。了解即可。