我们知道Java是一门跨平台的语言,我们编写的Java代码会被编译成中间class文件以让Java虚拟机解析运行。而Java虚拟机规范仅仅描述了抽象的Java虚拟机,在实现具体的Java虚拟机时,仅指出了设计规范。Java虚拟机的实现必须体现规范中的内容,但仅在确有必要时才应该受制于这些规范。对于完整内容,可以查看原文档,以JDK7为例,可查看https://docs.oracle.com/javase/specs/jvms/se7/html/,或者《深入理解Java虚拟机 JVM高级特性与最佳实践》一书。完整的规范主要包含以下内容:

  • 第2章:概览Java虚拟机整体架构
  • 第3章:介绍如何将Java语言编写的程序转换为虚拟机指令集
  • 第4章:定义class文件格式。它是一种与硬件和操作系统无关的二进制格式,用来表示编译后的类和接口
  • 第5章:定义了Java虚拟机启动以及类和接口的加载、链接和初始化的过程
  • 第6章:定义了Java虚拟机指令集
  • 第7章:提供了一张以操作码值为索引的Java虚拟机操作码助记表

 本文只是大概记录项目需要了解的基础概念,着重在介绍Class文件格式上,为该系列后续内容做铺垫。

 Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分割符。每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。

 每一个Class文件对应于一个如下所示的ClassFile结构体:

涉及到的内容包括:

  • magic:魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
  • minor_version、major_version:副版本号和主版本号,minor_version和major_version的值分别表示Class文件的副、主版本。一个Java虚拟机实例只能支持特定范围内的主版本号(Mi至Mj)和0至特定范围内(0至m)的副版本号。
  • constant_pool_count:常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
  • constant_pool[]:常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。
  • access_flags:访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。
  • this_class:类索引
  • super_class:父类索引
  • interfaces_count:接口计数器,interfaces_count的值表示当前类或接口的直接父接口数量
  • interfaces[]:接口表,在interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。
  • fields_count:字段计数器,fields_count的值表示当前Class文件fields[]数组的成员个数。
  • fields[]:字段表,fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
  • methods_count:方法计数器,methods_count的值表示当前Class文件methods[]数组的成员个数。
  • methods[]:方法表,methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
  • attributes_count:属性计数器,attributes_count的值表示当前Class文件attributes表的成员个数。
  • attributes[]:属性表

 可用jdk自带的javap命令对class文件进行反编译,以查看内容,如下代码:

public class Ex {

    public void judgeAge(int age) {
        int step = 0;
        if (age > 18) {
            step++;
            System.out.println("a litter old");
        } else {
            System.out.println("a litter cute");
            step++;
        }
        System.out.println(step);
    }

    public static void main(String[] args) {
        Ex ex = new Ex();
        ex.judgeAge(16);
    }
}

执行 javap -verbose -p Ex.class的结果为

Classfile Ex.class
  Last modified 2019-11-29; size 788 bytes
  MD5 checksum 8b5d8ebf38c4441fe7150c10da31ce1b
  Compiled from "Ex.java"
public class Ex
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #34            // a litter old
   #4 = Methodref          #35.#36        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #37            // a litter cute
   #6 = Methodref          #35.#38        // java/io/PrintStream.println:(I)V
   #7 = Class              #39            // Ex
   #8 = Methodref          #7.#31         // Ex."<init>":()V
   #9 = Methodref          #7.#40         // Ex.judgeAge:(I)V
  #10 = Class              #41            // java/lang/Object
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               LEx;
  #18 = Utf8               judgeAge
  #19 = Utf8               (I)V
  #20 = Utf8               age
  #21 = Utf8               I
  #22 = Utf8               step
  #23 = Utf8               StackMapTable
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               args
  #27 = Utf8               [Ljava/lang/String;
  #28 = Utf8               ex
  #29 = Utf8               SourceFile
  #30 = Utf8               Ex.java
  #31 = NameAndType        #11:#12        // "<init>":()V
  #32 = Class              #42            // java/lang/System
  #33 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #34 = Utf8               a litter old
  #35 = Class              #45            // java/io/PrintStream
  #36 = NameAndType        #46:#47        // println:(Ljava/lang/String;)V
  #37 = Utf8               a litter cute
  #38 = NameAndType        #46:#19        // println:(I)V
  #39 = Utf8               Ex
  #40 = NameAndType        #18:#19        // judgeAge:(I)V
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
  #47 = Utf8               (Ljava/lang/String;)V
{
  public Ex();
    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   LEx;

  public void judgeAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_1
         3: bipush        18
         5: if_icmple     22
         8: iinc          2, 1
        11: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        14: ldc           #3                  // String a litter old
        16: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        19: goto          33
        22: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: ldc           #5                  // String a litter cute
        27: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: iinc          2, 1
        33: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: iload_2
        37: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        40: return
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 8
        line 7: 11
        line 9: 22
        line 10: 30
        line 12: 33
        line 13: 40
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      41     0  this   LEx;
            0      41     1   age   I
            2      39     2  step   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 22
          locals = [ int ]
        frame_type = 10 /* same */

  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: new           #7                  // class Ex
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        16
        11: invokevirtual #9                  // Method judgeAge:(I)V
        14: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            8       7     1    ex   LEx;
}
SourceFile: "Ex.java"

下面对后续需要接触到的几项内容做说明。

1. 数据项

 Class文件中有两种数据类型,分别是无符号数和表:

  • 无符号数:属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节
  • 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾

2. 访问和修饰符标识

 带有 ACC_SYNTHETIC 标志的部分,意味着它是由编译器自己产生的而不是由程序员编写的源代码生成的。有该标志的类、属性、方法等不会在源码中显示。

3. 类型描述符

 基本类型的描述是单个字符:

  • Z表示 boolean
  • C表示 char
  • B表示 byte
  • S表示 short
  • I 表示 int
  • F 表示 float
  • J 表示 long
  • D 表示 double
  • 一个类类型的描述符是这个类的内部名,前面加上字符 L,后面跟有一个分号。例如,String 的类型描述符为 Ljava/lang/String;。而一个数组类型的 述符是一个方括号后面跟有该数组元素类型的描述符。

4. 方法描述符

 方法描述符是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。

 方法描述符以左括号开头,然后是每个形参的类型描述,然后是一个右括号,接下来是返回类型的类型描述符,如果该方法返回void,则是 V,表示方法描述中不包含方法的名字或参数名,可看如下例子:

5. 指令

 Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。

 常见的指令如下:

  • 字段访问指令:getfield,putfield,getstatic,pustatic
  • 比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
  • 跳转指令:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull
  • 比较条件跳转指令:if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne
  • 多条件分支跳转:tableswitch和lookupswitch
  • 无条件跳转:goto
  • 函数调用与返回指令
  • 函数调用指令:invokevirtual,invokeinterface,invokespecial,invokestatic,invokedynamic;
  • 函数返回:需要将返回值压入调用者操作数栈,需要使用xreturn指令(x可以是i,l,f,d,a或空)

 PS:笔者个人习惯使用Bytecode Outline进行反编译,这款插件输出的内容可读性会高点,上面的内容输出下:

// class version 52.0 (52)
// access flags 0x21
public class Ex {

  // compiled from: Ex.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LEx; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public judgeAge(I)V
   L0
    LINENUMBER 4 L0
    ICONST_0
    ISTORE 2
   L1
    LINENUMBER 5 L1
    ILOAD 1
    BIPUSH 18
    IF_ICMPLE L2
   L3
    LINENUMBER 6 L3
    IINC 2 1
   L4
    LINENUMBER 7 L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "a litter old"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    GOTO L5
   L2
    LINENUMBER 9 L2
   FRAME APPEND [I]
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "a litter cute"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L6
    LINENUMBER 10 L6
    IINC 2 1
   L5
    LINENUMBER 12 L5
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ILOAD 2
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L7
    LINENUMBER 13 L7
    RETURN
   L8
    LOCALVARIABLE this LEx; L0 L8 0
    LOCALVARIABLE age I L0 L8 1
    LOCALVARIABLE step I L1 L8 2
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 16 L0
    NEW Ex
    DUP
    INVOKESPECIAL Ex.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 17 L1
    ALOAD 1
    BIPUSH 16
    INVOKEVIRTUAL Ex.judgeAge (I)V
   L2
    LINENUMBER 18 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE ex LEx; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}
02-12 06:37