一步一步学习Java虚拟机(一) 输入之字节码文件

Feb 9, 2017


Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。Java语言编译程序将Java源文件编译成字节码。Java虚拟机接收字节码,将其解释为具体平台上的机器指令,而后执行。

如果将Java虚拟机看做一个黑盒,那么从上一句话我们可以得到Java虚拟机的初印象:

Java虚拟机的初印象
Java虚拟机的初印象

在进入Java虚拟机的内部之前,先来了解输入即编译成字节码的class文件。

字节码文件

在MacOS机器上安装Java 1.7, Java环境信息如:

$ java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

先常见一个HelloWorld.java文件,内容如:

public class HelloWorld {

  private String helloStr = "Hello World!";

  public String sayHello(String name) {
      return helloStr + name + "!";
  }

  public static void main(String [] args) {
    HelloWorld helloWorld =  new HelloWorld();
    System.out.println(helloWorld.sayHello("Wendll"));
  }
}

使用命令javac HelloWorld.java来编译源文件并生成字节码文件HelloWorld.class。字节码文件是一种二进制的类文件,按照JVM规范规定的class文件的结构应为:

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];
}

这里u2,u4代表2 字节,4字节, 而cp_info, field_info, method_info, attribute_info代表其他结构体。得知class文件是一个这样的结构体,我们便可以使用任意语言的文件读取程序去解析这个字节码文件。这里我用Java的DataInputStream来读取这个文件的一部分内容,如:

FileInputStream inputStream = new FileInputStream("HelloWorld.class");
DataInputStream stream = new DataInputStream(inputStream);
System.out.println(String.format("%X",stream.readInt())); // magic: CAFEBABE
System.out.println(String.format("%X",stream.readUnsignedShort())); //minor_version: 0
System.out.println(stream.readUnsignedShort()); //major_version: 51
System.out.println(stream.readUnsignedShort()); //constant_pool_count: 53

常量池(constant_pool)

常量池是一个变长的数组,其结构体非常简单

cp_info {
    u1 tag;
    u1 info[];
}

从tag可以得到此cp_info的类型,cp_info的类型是约定好的,目前有14种,如CONSTANT_Utf8是1, CONSTANT_Integer是3, CONSTANT_String是8, CONSTANT_Class是7,CONSTANT_Fieldref是9, CONSTANT_Methodref是10等等。具体参考Constant pool tags

这些tag的不同意味着后续的info数组的长度是不一样的, 如CONSTANT_Utf8_info用来表示字符串常量:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

再如CONSTANT_Class用来表示一个类或接口,其对应的是info数组的长度是2, 因为其对应的结构体是CONSTANT_Class_info:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_Long与对应的info数组的长度是8,其对应的结构体是:

CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

而对于CONSTANT_Methodref,其对应的info数组长度是4,其对应的结构体是:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

这里的class_index的值是指向常量池数组的某一个元素,这个元素必须是CONSTANT_Class_info结构,也注意这里的要求必须指向的是class而不是interface。

疑问:如何从字节码文件中区分是class还是interface呢?从CONSTANT_Class_info结构是无法区分的。通过access_flags可以区分。access_flags

name_and_type_index也指向常量池数组的某一个元素,这个元素必须是CONSTANT_NameAndType_info结构,此结构中包含名字和描述符:

  • 如果name是以 ‘<’ 开头,则其为方法,代表着实例初始化方法,返回值为void。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,如可以表示为`"":()V`。

  • 基本类型都有对应的大写字符表示,如C表示Char, D表示Double, Z表示Boolean, B表示Byte。

  • 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()”之内。如方法void inc()的描述符为”()V”,方法java.lang.String.toString()的描述符为”()Ljava/lang/String;”。

  • 对于数组类型,每一个维度将使用一个前置的”[“字符来描述,如一个定义的”java.lang.String[][]”类型的二维数组,将被记录为:”[[Ljava/lang/String;”,一个整型数组”int[]”将被记录为”[I”

  • 对象类型则用字符L加对象的全限定名来表示,如Ljava/lang/Object

access_flags

可以设置为ACC_PUBLIC,ACC_FINAL, ACC_SUPER, ACC_INTERFACE, ACC_ABSTRACT, ACC_SYNTHETIC, ACC_ANNOTATION, ACC_ENUM。

如设置了ACC_INTERFACE其为接口,如果没有设置ACC_INTERFACE,其为class。

怎么区分是不是抽象类(abstract class)呢?

this_class

this_class是一个索引,指向常量池数组中的一个元素,这个元素必须是CONSTANT_Class_info结构。

super_class

super_class可以为0或一个索引,指向常量池中为CONSTANT_Class_info结构的一个元素。

interfaces, fields与methods

interfaces数组只容纳那些直接出现在类声明的implements子句或者接口声明的extends子句中的父接口。

fields数组只列出在文件中由类或者接口声明了的字段才,不列出从超类或者父接口继承而来的字段。但fields列表可能会包含在对应的Java源文件中没有叙述的字段,这是因为Java编译器可以会在编译时向类或者接口添加字段。fileds数组中的每一个元素的结构如下:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

methods数组中列出该类或者接口中所声明的方法的描述。其描述结构与fields的描述结构一样,只是access_flags的值不一样,其可以设置为如ACC_ABSTRACT,ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED,ACC_STATIC,ACC_FINAL,ACC_SYNCHRONIZED等。

attributes

在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。attribute_info的结构如下:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

虚拟机预定义的属性有如:

  • Code,Java代码编译成的字节码指令

  • ConstantValue,final关键字定义的常量池

  • Exceptions,方法抛出的异常

  • LineNumberTable, Java源码的行号与字节码指令的对应关系

  • LocalVariableTable, 方法的局部便狼描述

  • Signature, 用于支持泛型情况下的方法签名

拿Code属性举例,其结构定义为Code_attribute,格式如:

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

Code_attribute中也可以包含attribute_info,如LineNumberTable,LocalVariableTable, LocalVariableTypeTable, StackMapTable。

字节码反汇编(javap)

javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。

常用命令选项:

  • -c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。
  • -verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本

使用命令javap -c -v HelloWorld即可查看HelloWorld.class文件的字节码相关信息。

可以查看到的基本信息如:

Last modified Feb 11, 2017; size 964 bytes
MD5 checksum d2ae1ea5de75d19acbe8a0c18d851298
Compiled from "HelloWorld.java"
public class HelloWorld
  SourceFile: "HelloWorld.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

常量池如:

Constant pool:
   #1 = Methodref          #15.#35        //  java/lang/Object."<init>":()V
   #2 = String             #36            //  Hello World!
   #3 = Fieldref           #9.#37         //  HelloWorld.helloStr:Ljava/lang/String;
   #4 = Class              #38            //  java/lang/StringBuilder
   #5 = Methodref          #4.#35         //  java/lang/StringBuilder."<init>":()V
   #6 = Methodref          #4.#39         //  java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = String             #40            //  !
   #8 = Methodref          #4.#41         //  java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Class              #42            //  HelloWorld
  #10 = Methodref          #9.#35         //  HelloWorld."<init>":()V
  #11 = Fieldref           #43.#44        //  java/lang/System.out:Ljava/io/PrintStream;
  #12 = String             #45            //  Wendll
  #13 = Methodref          #9.#46         //  HelloWorld.sayHello:(Ljava/lang/String;)Ljava/lang/String;
  #14 = Methodref          #47.#48        //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #15 = Class              #49            //  java/lang/Object
  #16 = Utf8               helloStr
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               LHelloWorld;
  #25 = Utf8               sayHello
  #26 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #27 = Utf8               name
  #28 = Utf8               main
  #29 = Utf8               ([Ljava/lang/String;)V
  #30 = Utf8               args
  #31 = Utf8               [Ljava/lang/String;
  #32 = Utf8               helloWorld
  #33 = Utf8               SourceFile
  #34 = Utf8               HelloWorld.java
  #35 = NameAndType        #18:#19        //  "<init>":()V
  #36 = Utf8               Hello World!
  #37 = NameAndType        #16:#17        //  helloStr:Ljava/lang/String;
  #38 = Utf8               java/lang/StringBuilder
  #39 = NameAndType        #50:#51        //  append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #40 = Utf8               !
  #41 = NameAndType        #52:#53        //  toString:()Ljava/lang/String;
  #42 = Utf8               HelloWorld
  #43 = Class              #54            //  java/lang/System
  #44 = NameAndType        #55:#56        //  out:Ljava/io/PrintStream;
  #45 = Utf8               Wendll
  #46 = NameAndType        #25:#26        //  sayHello:(Ljava/lang/String;)Ljava/lang/String;
  #47 = Class              #57            //  java/io/PrintStream
  #48 = NameAndType        #58:#59        //  println:(Ljava/lang/String;)V
  #49 = Utf8               java/lang/Object
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               toString
  #53 = Utf8               ()Ljava/lang/String;
  #54 = Utf8               java/lang/System
  #55 = Utf8               out
  #56 = Utf8               Ljava/io/PrintStream;
  #57 = Utf8               java/io/PrintStream
  #58 = Utf8               println
  #59 = Utf8               (Ljava/lang/String;)V

Code属性如:

{
  public HelloWorld();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Hello World!
         7: putfield      #3                  // Field helloStr:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 1: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  this   LHelloWorld;

  public java.lang.String sayHello(java.lang.String);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: new           #4                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: getfield      #3                  // Field helloStr:Ljava/lang/String;
        11: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: aload_1
        15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: ldc           #7                  // String !
        20: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        26: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      27     0  this   LHelloWorld;
               0      27     1  name   Ljava/lang/String;

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #9                  // class HelloWorld
         3: dup
         4: invokespecial #10                 // Method "<init>":()V
         7: astore_1
         8: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: ldc           #12                 // String Wendll
        14: invokevirtual #13                 // Method sayHello:(Ljava/lang/String;)Ljava/lang/String;
        17: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      21     0  args   [Ljava/lang/String;
               8      13     1 helloWorld   LHelloWorld;
}

Code属性中的代码是Java指令,是基于栈的体系结构,JVM就是解析这些指令,把它翻译成相应的二进制机器码来执行。Java总共有200多条指令,不过很多都是重复的。其中一个原因可能是基于网络是Java一个非常重要的特性,而且Java在设计之初就认为字节码是要在网络中传输的,为了减少网络传输流量,字节码就要尽量设计精简、紧凑。因而Java增加了很多重复指令,比如尽量减少操作数,因而我们会发现Java的很多指令都是没有操作数的;并且指令中的操作数基本上都是当无法将值放到栈中的数据,比如局部变量的索引号和常量池中的索引号。

参考资料