Java虚拟机—Class文件结构

Java虚拟机—Class文件结构

前言:

在前几篇文章中:

Java虚拟机——字节码、机器码和JVM
Java虚拟机——类加载机制和类加载器
Java虚拟机—堆、栈、运行时数据区

我们大概介绍了JVM、字节码、类加载器和JVM运行时数据区的概念,现在让我们进入JVM的重要部分—.class文件的结构。所以本篇文章的主题主要包含以下2个部分:

1.Java语言的平台无关性和JVM的语言无关性

2.字节码.class文件的结构


1.Java语言的平台无关性和JVM的语言无关性

Java语言的平台无关性

《深入理解Java虚拟机-第二版》中第6章开头就写到:

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。

正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”

Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。

JVM的语言无关性

Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:

"In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages" 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。

时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。

所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。。。

2.字节码.class文件的结构

根据Java虚拟机的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有2种数据类型:无符号数和表。

当然无论是无符号数还是表,Class文件都是以8位(8bit),一个字节为单位存储的,各个数据项目紧密无间隔排列的二进制流。当数据项长度超过8位时,按照高位在前(Big Endian)的方式分隔成若干个8位字节存储。

无符号数用u表示后面跟1、2、4、8代表1个字节、2个字节、4个字节、8个字节。无符号数用来描述数字、索引引用、数量值、字符串值。

表则是由无符号数或者其他表作为数据复合而成的数据类型,所有表都习惯以_info结尾。

整个Class文件实质上就是一张表,其中的数据项由各个子表和无符号数构成。Class文件的格式如下:

此处需要注意的是,由于class文件没有任何分隔符号,所有.class文件中所有的数据项(表或无符号数)都是按照图表中的顺序依次排列好的,所以我们可以在.class文件中依照字节的顺序来查看对应数据项的详细信息。

这里我们以一个.class文件为例,看看其具体的字节信息,源码如下:

package JustCoding.Practise;

public class ConstantPool {

    private static String a = "Class";

    public int VERSION = 100;

    private static void test1(String s){
        String b = "Method ";
        String c = b + s;
        System.out.println("合并后的字符串:"+c);
    }
    public static void main(String[] args){
        test1(a);
    }
}

用vim打开其.class文件查看其16进制文件如下:

魔数magic

如上图所示,是ConstantPool.class文件的16进制表示,前4个字节为ca fe ba be,这个即为表6-1中的magic,magic译为“魔数”,在.class文件的头4个字节,它的唯一作用是确定这个文件是否是能够被虚拟机识别的class文件,其值是固定的为0xCAFEBABE(咖啡宝贝),这个也是Java语言中一段有意思的“黑”历史了,哈哈

版本声明major_version、minor_version

紧挨着魔数后的第5、6两个字节存储的是minor_version,即Java的次版本号,第7、8两个字节是major_version主版本号。可以看见这里此版本号为0x0000,主版本号为0x0034。

每个Java版本都有对应的主、次版本号可以查询。
例子中的0x0034对应10进制的52,表示JDK的主版本号为1.8。

常量池计数项constant_pool_count

版本声明后,是一个2个字节的无符号数u2用于标志常量池容量,此处0x0040,等于10进制下的64,表明常量池中有63项常量。

(此处有个小设计,容量计数是从1开始而不是从0开始,故64-1=63)

常量池表constant_pool

接着就到了常量池表cp_info。此处常量池表就是之前文章Java虚拟机—堆、栈、运行时数据区中提到的,方法区中的运行时常量池。

5.1运行时常量池
运行时常量池(Runtime Constant Pool)是.class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,属于方法区的一部分。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口道虚拟机后,就创建对应的运行时常量池。常量池的作用是:
存放编译器生成的各种字面量和符号引用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。
字面量(Literal),通俗理解就是Java中的常量,如文本字符串、声明为final的常量值等。
符号引用(Symbolic References)则是属于编译原理中的概念,包括了下面三类常量:
1.类和接口的全限定名
2.字段的名称和描述符
3.方法的名称和描述符

常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用class空间最大的一个数据项。

因为常量池中常量的数量不是固定的,所以需要2字节的无符号u2(constant_pool_count)代表常量池容量计数值(此处有个小设计,容量计数是从1开始而不是从0开始)。

常量池中的每一项常量都是一个表。每个常量项表中第一位是一个u1类型的标志位,用于标志常量的类型,具体各个常量表如下图所示(目前有14种类型的常量,表中只列了11项):

图片引用自:http://www.sohu.com/a/131458551_504186

到此,我们来看一下用javap -v ConstantPool.class反编译一下.class文件来看看字节码的组成情况:

Classfile xxx/.../ConstantPool.class
  Last modified 2018年9月20日; size 1063 bytes
  MD5 checksum 024d748f4dc1776164f6c3e8e19cf95b
  Compiled from "ConstantPool.java"
public class JustCoding.Practise.ConstantPool
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #14                         // JustCoding/Practise/ConstantPool
  super_class: #15                        // java/lang/Object
  interfaces: 0, fields: 2, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #15.#39        // java/lang/Object."<init>":()V
   #2 = Fieldref           #14.#40        // JustCoding/Practise/ConstantPool.VERSION:I
   #3 = String             #41            // Method
   #4 = Class              #42            // java/lang/StringBuilder
   #5 = Methodref          #4.#39         // java/lang/StringBuilder."<init>":()V
   #6 = Methodref          #4.#43         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Methodref          #4.#44         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Fieldref           #45.#46        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = String             #47            // 合并后的字符串:
  #10 = Methodref          #48.#49        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Fieldref           #14.#50        // JustCoding/Practise/ConstantPool.a:Ljava/lang/String;
  #12 = Methodref          #14.#51        // JustCoding/Practise/ConstantPool.test1:(Ljava/lang/String;)V
  #13 = String             #52            // Class
  #14 = Class              #53            // JustCoding/Practise/ConstantPool
  #15 = Class              #54            // java/lang/Object
  #16 = Utf8               a
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               VERSION
  #19 = Utf8               I
  #20 = Utf8               <init>
  #21 = Utf8               ()V
  #22 = Utf8               Code
  #23 = Utf8               LineNumberTable
  #24 = Utf8               LocalVariableTable
  #25 = Utf8               this
  #26 = Utf8               LJustCoding/Practise/ConstantPool;
  #27 = Utf8               test1
  #28 = Utf8               (Ljava/lang/String;)V
  #29 = Utf8               s
  #30 = Utf8               b
  #31 = Utf8               c
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               args
  #35 = Utf8               [Ljava/lang/String;
  #36 = Utf8               <clinit>
  #37 = Utf8               SourceFile
  #38 = Utf8               ConstantPool.java
  #39 = NameAndType        #20:#21        // "<init>":()V
  #40 = NameAndType        #18:#19        // VERSION:I
  #41 = Utf8               Method
  #42 = Utf8               java/lang/StringBuilder
  #43 = NameAndType        #55:#56        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #44 = NameAndType        #57:#58        // toString:()Ljava/lang/String;
  #45 = Class              #59            // java/lang/System
  #46 = NameAndType        #60:#61        // out:Ljava/io/PrintStream;
  #47 = Utf8               合并后的字符串:
  #48 = Class              #62            // java/io/PrintStream
  #49 = NameAndType        #63:#28        // println:(Ljava/lang/String;)V
  #50 = NameAndType        #16:#17        // a:Ljava/lang/String;
  #51 = NameAndType        #27:#28        // test1:(Ljava/lang/String;)V
  #52 = Utf8               Class
  #53 = Utf8               JustCoding/Practise/ConstantPool
  #54 = Utf8               java/lang/Object
  #55 = Utf8               append
  #56 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #57 = Utf8               toString
  #58 = Utf8               ()Ljava/lang/String;
  #59 = Utf8               java/lang/System
  #60 = Utf8               out
  #61 = Utf8               Ljava/io/PrintStream;
  #62 = Utf8               java/io/PrintStream
  #63 = Utf8               println
{
  public int VERSION;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC

  public JustCoding.Practise.ConstantPool();
    descriptor: ()V
    flags: (0x0001) 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: bipush        100
         7: putfield      #2                  // Field VERSION:I
        10: return
      LineNumberTable:
        line 3: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LJustCoding/Practise/ConstantPool;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #11                 // Field a:Ljava/lang/String;
         3: invokestatic  #12                 // Method test1:(Ljava/lang/String;)V
         6: return
      LineNumberTable:
        line 15: 0
        line 16: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #13                 // String Class
         2: putstatic     #11                 // Field a:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 5: 0
}
SourceFile: "ConstantPool.java"

可以看到,minor version: 0 major version: 52;Constant pool共有63项。和我们之前看16进制码时是一一对应的。

访问标志access_flags

在常量池表后面的两个字节代表访问标志,用于标志类或接口层次的访问信息。如:这个Class文件是类还是接口?是否是public?是否为抽象的abstract?是否为final的等。

类索引this_class、父类索引super_class和接口索引集合intefaces

类索引用于确定此类的全限定名称:JustCoding/Practise/ConstantPool,父类索引super_class用于确定这个类父类的全限定名:java/lang/Object。接口索引集合intefaces用来描述这个类实现了哪些接口。

类索引this_class、父类索引super_class都是一个u2类型的数据,接口索引集合包含一个u2类型的接口计数项intefaces_count和若干个u2类型的数据集合。

字段表集合fields_count+field_info

字段表集合用于描述接口或类中声明的变量。字段filed包括类变量、实例变量,但不包括方法内部声明的局部变量。字段表结构如下:

字段表集合中第一项是access_flags,需要注意的是,这里的access_flags和之前类中的access_flags类似,是一个u2类型的数据,表示字段访问标记,可以设置9个标记位用于标记字段是否为:public,private,protected,static,final,volatile,transient,enum,是否由编译器自动产生。

然后是name_index和descriptor_index,他们分别代表字段的简单名称和字段OR方法的描述符。方法表集合用于存储此类或接口中包含的方法,表结构和字段表类似。简单名称是指没有类型修饰、没有参数修饰的字段OR方法名称。简单名称很好理解,在例子中有:a ,VERSION ,test1。描述符descriptor则稍微麻烦点,描述符的作用是用来描述字段的数据类型、方法参数列表和返回值。例子中描述符有:I , ()V,([Ljava/lang/String;)V这几个。

最后是属性表集合attributes_count+attribute_info,用于记录一些属性。

方法表集合methods_count+method_info

和字段表集合类似,此处需要注意的是,通过访问标志accessflags、名称索引nameindex、描述符索引descriptorindex来定义了方法,方法的实际代码存放在属性表attribute_info中的“Code”属性中。

属性表集合attributes_count+attribute_info

属性表在前面已经出现了多次,在class文件、字段表、方法表中都可以包含自己的属性表集合用于描述自己特定的属性。class文件中其他的数据项对顺序、长度和内容要求十分严格,而对属性表则相对宽松,不再要求属性表具有严格顺序,且只要不和已有属性重名,即可向属性表中写入自己定义的属性。JVM运行时会忽略掉它所不认识的属性。

Java虚拟机规范(Java8)中预定义了23种属性,按照不同分类,大致可分为3类:

熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。所以下一篇文章我们就来对着反编译后的.class文件来继续学习和讲解JVM字节码指令,毕竟JVM指令才是整个JVM的核心。

编辑于 2018-09-20

文章被以下专栏收录