JVM class解析(一)——文件格式

卢利如

前言

我们知道,java代码运行,需要先编译成class文件,再经由JVM加载来解释执行。那class文件中,究竟存放了哪些信息,JVM又是如何通过加载这些信息来执行我们的java代码的。通过了解class文件,有助于我们理解JVM的运行机制。

class文件概述

每个class文件对应一个具体类或者接口定义。它由8字节为单位的字节流组成,这也是我们称之为“字节码”的由来,而16、32、64位的数据分别以2、4、8字节来表示。

class文件结构

class文件结构如下,u2、u4分别代表2、4个字节长度:

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:魔数,占用4个字节,固定值为“0xCAFEBABE”,用于标识这是一个class文件。

minor_versionmajor_version:class文件次、主版本号,分别占用2个字节,一般高版本的JVM能够加载低版本class文件,反之不行。具体的对应如下:

JDK版本 16进制版本号
JDK8 00 00 00 34
JDK7 00 00 00 33
JDK6 00 00 00 32
JDK5 00 00 00 31
JDK1.4 00 00 00 30
JDK1.3 00 00 00 2F
JDK1.2 00 00 00 2E
JDK1.1 00 00 00 2D

constant_pool_countconstant_pool:常量池相关数据,下面会详细介绍。

access_flags:访问标志,占用2个字节。用来表明该class文件中定义的是类还是接口,访问修饰符是public或其他等信息。

标志名 含义
ACC_PUBLIC 0x0001 是否public
ACC_FINAL 0x0010 是否final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令,JDK1.2以后的编译器编译出来的class文件该标志都为真
ACC_INTERFACE 0x0200 是否接口
ACC_ABSTRACT 0x0400 是否abstract
ACC_SYNTHETIC 0x1000 是否由用户代码生成
ACC_ANNOTATION 0x2000 是否注解类
ACC_ENUM 0x4000 是否枚举类

比如:0x0021,代表含有ACC_PUBLICACC_SUPER标志,即 public class

this_class:常量池中,类名的索引,占用2个字节。比如this_class=0x0001。则表示指向常量池中的第一个常量表。

super_class:常量池中,父类名的索引,占用2个字节。

interfaces_countinterfaces:实现的接口相关数据,下面会详细介绍。

fields_countfields:拥有field相关数据,下面会详细介绍。

methods_countmethods:拥有method相关数据,下面会详细介绍。

attributes_countattributes:属性相关数据,下面会详细介绍。

常量池

constant_pool_count:常量池计数,该值等于constant_pool表中的成员数+1。

constant_pool每一项的数据结构如下:

cp_info {  
    u1 tag;
    u1 info[]; 
}

tag占用1字节,标识该项的类型。

常量类型
CONSTANT_Utf8 1
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_Class 7
CONSTANT_String 8
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_NameAndType 12
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

有人会奇怪,tag的值不存在2、13、14、17,那是因为JVM在发展的过程中,这些类型的常量被舍弃了。

下面介绍下这几种常量类型:

CONSTANT_Utf8

CONSTANT_Utf8类型用于表示字符串常量,结构如下:

CONSTANT_Utf8_info {  
    u1 tag;
    u2 length;
    u1 bytes[length];
}
  • length:bytes[]数组的长度。
  • bytes[]:字符串经编码之后的byte数组,编码格式为改进过的UTF-8。

CONSTANT_Integer、CONSTANT_Float

CONSTANT_IntegerCONSTANT_Float类型分别用来保存integer、float类型常量。

CONSTANT_Integer_info {  
    u1 tag;
    u4 bytes; 
}
CONSTANT_Float_info {  
    u1 tag;
    u4 bytes; 
}

CONSTANT_Long、CONSTANT_Double

CONSTANT_LongCONSTANT_Double类型分别用来保存long、double类型常量。

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

CONSTANT_Class

CONSTANT_Class用于表示类和接口。

CONSTANT_Class_info {  
    u1 tag;
    u2 name_index;
}

name_index为CONSTANT_Utf8类型常量项的索引,里面存储了类或接口的内部表示形式。如int[][]的内部表现形式为[[I,而Thread[]则表现为[Ljava/lang/Thread;

CONSTANT_String

CONSTANT_String用于表示 java.lang.String 类型的常量对象。

CONSTANT_String_info {  
    u1 tag;
    u2 string_index;
}

string_index为CONSTANT_Utf8类型常量项的索引。

CONSTANT_Fieldref、CONSTANT_Methodref、CONSTANT_InterfaceMethodref

CONSTANT_FieldrefCONSTANT_MethodrefCONSTANT_InterfaceMethodref用于表示字段、类方法和接口方法。结构如下:

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

class_index为CONSTANT_Class类型常量项的索引,分别表示字段、方法是该类、接口的成员。

name_and_type_index为CONSTANT_NameAndType类型常量项的索引。

CONSTANT_NameAndType

CONSTANT_NameAndType用于表示字段、方法的描述符。

CONSTANT_NameAndType_info {  
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

descriptor_index为CONSTANT_Utf8类型常量项的索引,里面存储了字段、方法的描述符。

字段描述符,比如int 实例变量的描述符是“I”;java.lang.Object 的实例描述符是 “Ljava/lang/Object;”,“double[][][]”的描述符为“[[[D”;

方法描述符,比如Object mymethod(int i, double d, Thread t)的描述符为 (IDLjava/lang/Thread;)Ljava/lang/Object;。

CONSTANT_MethodHandle

CONSTANT_MethodHandle用于表示方法handle。

CONSTANT_MethodHandle_info {  
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

reference_kind该值范围为[1,9],用于表示handle的类型。

reference_index为常量池项的索引,根据reference_kind值不同,指向不同类型的常量池项。当reference_kind为1、2、3、4时,为CONSTANT_Fieldref的索引值;当reference_kind为5、6、7、8时,为CONSTANT_Methodref的索引值;当reference_kind为9时,为CONSTANT_InterfaceMethodref的索引值。

CONSTANT_MethodType

CONSTANT_MethodType用于表示方法类型。

CONSTANT_MethodType_info {  
    u1 tag;
    u2 descriptor_index;
}

descriptor_index为CONSTANT_Utf8类型常量项的索引,里面存储了方法描述符的字符串。

CONSTANT_InvokeDynamic

CONSTANT_InvokeDynamic是Java7新引入的一种类型,主要用于invokedynamic指令。

CONSTANT_InvokeDynamic_info {  
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

bootstrap_method_attr_index为class文件中attributes属性的索引。

name_and_type_index为CONSTANT_MethodType类型常量项的索引,表示方法名和方法描述符。

interface

interfaces_count:该类直接实现的接口数或是该接口的直接父接口数。

interfaces:列表每一项保存常量池中CONSTANT_Class类型常量的索引。

field

fields_count:该类或者接口所拥有的字段数。

fields:列表每一项为field_info数据,field_info结构如下:

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

access_flags:用于表示字段的访问属性,有如下几种类型:

名称 描述
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_SYNTHETIC 0x1000 由编译器自动生成
ACC_ENUM 0x4000 enum,字段为枚举类型

name_index:CONSTANT_Utf8类型常量项的索引,里面存储了字段名称。

descriptor_index:CONSTANT_Utf8类型常量项的索引,里面存储了字段描述符。

attributes_count:当前字段拥有的attribute数量。

attributes:列表每一项为attribute_info结构数据,下面会详细描述。

method

methods_count:该类或接口拥有的方法数。

methods:列表每一下为method_info数据,method_info结构如下:

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

access_flags:用于表示方法的访问属性,有如下几种类型:

名称 描述
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_SYNCHRONIZED 0x0020 synchronized
ACC_BRIDGE 0x0040 bridge,方法由编译器产生
ACC_VARARGS 0x0080 该方法带有变长参数
ACC_NATIVE 0x0100 native
ACC_ABSTRACT 0x0400 abstract
ACC_STRICT 0x0800 strictfp
ACC_SYNTHETIC 0x1000 方法由编译器生成

name_index:CONSTANT_Utf8类型常量项的索引,里面存储了方法名称。

descriptor_index:CONSTANT_Utf8类型常量项的索引,里面存储了方法描述符。

attributes_count:当前方法拥有的attribute数量。

attributes:列表每一项为attribute_info结构数据,下面会详细描述。

attribute

attributes_count:当前class文件的attribute_info结构数据的数量。

attribute_info的数据结构如下:

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

attribute_name_index为CONSTANT_Utf8类型常量项的索引,表示attribute的名称。

所有的attribute名称如下:

  • ConstantValue
  • Code
  • StackMapTable
  • Exceptions
  • InnerClasses
  • EnclosingMethod
  • Synthetic
  • Signature
  • SourceFile
  • SourceDebugExtension
  • LineNumberTable
  • LocalVariableTable
  • LocalVariableTypeTable
  • Deprecated
  • RuntimeVisibleAnnotations
  • RuntimeInvisibleAnnotations
  • RuntimeVisibleParameterAnnotations
  • RuntimeInvisibleParameterAnnotations
  • AnnotationDefault
  • BootstrapMethods

attribute_length表示info的长度,单位为字节。

下面筛选一些对我们Java开发比较重要的属性进行介绍。

ConstantValue属性

ConstantValue属性表示一个常量field的值,出现在field_info结构中。

对于ConstantValue属性,它的attribute_length的值固定为2。

info的值为常量池项的索引。

Code属性

Code属性用于保存方法、类初始化方法、对象初始化方法内指令等相关信息。

Code属性的结构如下:

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

attribute_length的值为整个Code属性减去attribute_name_indexattribute_length的长度。

max_stack为当前方法执行时的最大栈深度,所以JVM在执行方法时,线程栈的栈帧大小是可以提前知道的。

max_locals为当前方法分配的局部变量个数,包括调用方式时传递的参数。long和double类型计数为2,其他为1。

code_length为方法编译后的字节码的长度。

code存放方法编译后的字节码。

exception_table_length表示exception_table的长度。

exception_table的每个成员为一个ExceptionHandler。

start_pcend_pc为异常处理字节码在code[]的索引值。当程序计数器在[start_pc, end_pc)内时,表示异常会被该ExceptionHandler捕获。

handler_pc表示ExceptionHandler的起点,为code[]的索引值。

catch_type为CONSTANT_Class类型常量项的索引,表示处理的异常类型。如果该值为0,则该ExceptionHandler会在所有异常抛出时会被执行,可以用来实现finally代码。

attributes_countattributes表示该exception_table拥有的attribute。

Exceptions属性

Exceptions属性表明了方法可能抛出的checked exception,出现在method_info结构中。

Exceptions属性结构如下:

Exceptions_attribute {  
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}

number_of_exceptions表示总共有几种异常。

exception_index_table列表每项为CONSTANT_Class常量项的索引,表示具体的异常类。

InnerClasses属性

InnerClasses属性用来描述内部类信息。

InnerClasses属性结构如下:

InnerClasses_attribute {  
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    {   u2 inner_class_info_index;
        u2 outer_class_info_index;
        u2 inner_name_index;
        u2 inner_class_access_flags;
    } classes[number_of_classes];
}

number_of_classes表示内部类的个数。

inner_class_info_indexCONSTANT_Class常量项的索引,指明内部类的类型。

outer_class_info_indexCONSTANT_Class常量项的索引,指明内部类对应的外部类的类型。

inner_name_indexCONSTANT_Utf8常量项的索引,表示内部类的名称。

inner_class_access_flags表示内部类的访问属性,具体取值如下:

名称 描述
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_INTERFACE 0x0200 是一个接口
ACC_ABSTRACT 0x0400 abstract
ACC_SYNTHETIC 0x1000 非源文件定义
ACC_ANNOTATION 0x2000 是注解
ACC_ENUM 0x4000 是枚举类

SourceFile属性

SourceFile属性用来指明源文件的名字。

SourceFile属性结构如下:

SourceFile_attribute {  
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

sourcefile_indexCONSTANT_Utf8常量项的索引。

SourceDebugExtension属性

SourceDebugExtension属性用于保存扩展调试信息,扩展调试信息,对JVM来说,没有用处。

LineNumberTable属性

LineNumberTable属性用于确定方法执行时,指令码对应的源文件行号。

LineNumberTable属性的结构如下:

LineNumberTable_attribute {  
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number;    
    } line_number_table[line_number_table_length];
}

start_pc为code[]数组元素的索引。

line_number为对应源文件代码的行号。

Deprecated属性

Deprecated属性表示类、接口、方法、字段被加上了@deprecated注解。

RuntimeVisibleAnnotations属性

RuntimeVisibleAnnotations属性用于保存类、字段或方法的运行时的可见注解,这些注解能够通过反射取得。

RuntimeVisibleAnnotations属性的结构如下:

RuntimeVisibleAnnotations_attribute {  
     u2 attribute_name_index;
     u4 attribute_length;
     u2 num_annotations;
     annotation annotations[num_annotations];
}

其中annotation的结构如下:

annotation {  
    u2 type_index;
    u2 num_element_value_pairs;
    {   u2            element_name_index;
        element_value value;
    } element_value_pairs[num_element_value_pairs];
}

type_indexCONSTANT_Utf8常量项的索引,表示注解类型。

num_element_value_pairs表明当前注解的键值对数量。

element_name_indexCONSTANT_Utf8常量项的索引,表示当前注解键值对的键名。

element_value表示当前注解键值对的值,结构如下:

element_value {  
    u1 tag;
    union {
        u2 const_value_index;

        {   u2 type_name_index;
            u2 const_name_index;
        } enum_const_value;

        u2 class_info_index;

        annotation annotation_value;

        {   u2            num_values;
            element_value values[num_values];
        } array_value;
    } value;
}

tag表明了值的类型,如下所示:

tag type
B byte
C char
D double
F float
I int
J long
S short
Z boolean
s String
e Enum
c Class
@ Annotation
[ Array

type_name_indexCONSTANT_Utf8常量项的索引,表示当前element_value结构所表示的枚举常量类型的内部形式的二进制名称。

const_name_indexCONSTANT_Utf8常量项的索引,表示当前element_value结构所表示的枚举常量 类型的简单名称。

annotation_value只有当tag为'@'时才会存在。这时element_value表示一个内部的注解。

array_value只有当tag为'['是才会存在。

RuntimeVisibleParameterAnnotations属性

RuntimeVisibleParameterAnnotations属性出现在method_info结构中,用于保存方法参数的注解。

RuntimeVisibleParameterAnnotations属性的结构如下:

RuntimeVisibleParameterAnnotations_attribute {  
    u2 attribute_name_index;
    u4 attribute_length;
    u1 num_parameters;
    {   u2         num_annotations;
        annotation annotations[num_annotations];
    } parameter_annotations[num_parameters];
}

num_parameters表示方法的参数个数。

num_annotations分别表示每个参数所拥有的注解个数。

annotations等同RuntimeVisibleAnnotations属性中出现的,描述注解的详情。

总结

JVM通过class文件这层中间层,将Java语言规范与JVM规范分隔开来,从而为JVM平台运行多种语言提供了基础。只要是符合class文件规范的,都可以被JVM加载运行,而不管是java、groovy、scala编译而来。

在下一篇文章中,我将介绍下java的方法在JVM里是如何执行。

参考资料

  1. 《深入理解Java虚拟机》
  2. The Java® Virtual Machine Specification