JVM学习--Class类文件结构

一个例子(下面的表根据这个例子讲解)

public class test{
    private int m;
    public int inc(){
        return m+1;
    }
}

经过javac test.java生成test.class。使用WinHex查看如下:
JVM学习--Class类文件结构

Class类文件结构

Class文件格式

u1~8代表 1 ~ 8个字节的无符号数,_info代表表

类型 名称 数量
u4 魔术 1
u2 次版本号 1
u2 主版本号 1
u2 常量池数量 1
cp_info constant_pool 常量池数量 - 1
u2 访问标识 1
u2 本类标识 1
u2 父类标识 1
u2 接口数量 1
u2 接口 接口数量
u2 字段数量 1
field_info 字段 字段数量
u2 方法数量 1
method_info 方法 方法数量
u2 属性数量 1
attribute_info 属性 属性数量

魔术

标识这个文件是java文件,固定为:

CA FE BA BE

cafe babe记为咖啡宝贝。

次版本号

现在好像基本都是

00 00

主版本号

00 34

34(16进制) = 52(10进制)

版本和版本号对应如下:

Java 1.2 uses major version 46
Java 1.3 uses major version 47
Java 1.4 uses major version 48
Java 5 uses major version 49
Java 6 uses major version 50
Java 7 uses major version 51
Java 8 uses major version 52
Java 9 uses major version 53
Java 10 uses major version 54
Java 11 uses major version 55

常量池计数

需要注意的是计数是从1开始的。0代表的是某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。

00 13

0x0013 -> 19 代表常量池中有19-1=18项常量,索引为1~18.

常量池

常量池中存放两大类常量:字面量(literal)和符合引用(Symbolic References)。
字面量接近于java语言层面的常量概念,如String,被声明为final的常量值等;
符合引用主要包含以下三种常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

当虚拟机运行时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址中。
这些表都有一个共同的特点,就是表的第一位是一个u1类型的标志位。

Constant Type Value 描述
CONSTANT_Class 7 类或接口的符号引用
CONSTANT_Fieldref 9 字段的符号引用
CONSTANT_Methodref 10 类中方法的符号引用
CONSTANT_InterfaceMethodref 11 接口中方法的符号引用
CONSTANT_String 8 字符串类型的字面量
CONSTANT_Integer 3 整型字面量
CONSTANT_Float 4 浮点型字面量
CONSTANT_Long 5 长整型字面量
CONSTANT_Double 6 双精度浮点型字面量
CONSTANT_NameAndType 12 字段或方法的部分符号引用
CONSTANT_Utf8 1 utf-8编码的字符串
CONSTANT_MethodHandle 15 方法处理句柄
CONSTANT_MethodType 16 方法类型
CONSTANT_InvokeDynamic 18 动态方法的调用点

由于每一项常量表都有自己的结构,这里就不展开讲。就讲第一个

0A

0A -> 10 对照上面的表发现是类中方法的符号引用。去查对应的结构为:

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

u1一个字节的tag已经读取了,接下来是u2两个字节的类索引

00 04

这个index代表指向常量池第4项,这里不能直接得到第4项还要接着往下看。

接下来是u2两个字节的字段或方法的部分符号的索引

00 0F

这个index指向常量池第15项,目前也无法得到o(╥﹏╥)o
这里刷下花招,用javap看看。发现上面的解析并没有错误。
JVM学习--Class类文件结构

访问标志

访问标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public等。

Flag Name Value 描述
ACC_PUBLIC 0x0001 标识是public类型
ACC_FINAL 0x0010 标识是final类型
ACC_SUPER 0x0020 标识允许使用invokespecial字节码指令,JDK1.2后默认为true
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 标识抽象类型
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码生成
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

access_flags中共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位一律为0。
使用到的标识value进行 运算,例如test类只用到了public修饰,所以ACC_PUBLIC 和 ACC_SUPER为真。所以值为0x0001 | 0x0020 = 0x0021
JVM学习--Class类文件结构

类索引、父类索引、接口索引集合

类索引和父类索引都是一个u2类型的数据,接口集合(包含接口数量+接口)是一组u2类型的数据的集合,Class由这3项数据确定类的继承关系。
类索引是类的全限定名。
父类索引是类的父类也就是extends。
接口数量+接口表明接口的个数和名称就是implements(如果这个类是接口则是extends)。
索引指向的是常量池里的CONSTANT_Class_info,这个索引最终会指向一个CONSTANT_Uft8_info类型的全限定名字符串。
除了java.lang.Object其他所有的父索引都不为0.

如果接口数量为0,后面的接口不再占用任何字节。

字段表集合

字段表用于描述接口或类中声明的变量。字段包含了类级变量和实例级变量,但不包括在方法内声明的变量。

字段表结构:

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

其中 access_flags 包含:这个很好理解,就不解释了。

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

name_index和descriptor_index 都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。简单名称指的是 没有任何修饰符的方法名或字段名,这个类中就是inc和m。

全限定名是把.换成/,且在最后加;例如java.lang.String --> java/lang/String;

描述符用来描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本数据类型及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。

FieldDescriptor:
    FieldType

FieldType:
    BaseType
    ObjectType
    ArrayType	

BaseType:
    B  byte
    C  char
    D  double
    F  float
    I   int
    J  long(注意区别)
    S String
    Z  boolean
    V void返回值

ObjectType:
    L ClassName ; 例如 `Ljava/lang/Object;`

ArrayType:
    [ ComponentType   保留一个前置的括号例如:int[][]   -->  [[I

ComponentType:
    FieldType

描述方法按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号里。
例如:void inc() --> ()V 再例如java.lang.String.copyValueOf(char data[], int offset, int count) ---> ([CII)Ljava/lang/String;

JVM学习--Class类文件结构

选定的浅蓝色就是这段内容,解读如下:1个字段,修饰符是2(private),字段名是5(m),描述是6(I).连起来就是private int m

descriptor_index;后还可能有属性表,用来描述额外的信息。如果将字段m的声明改为"final static int m = 123",那就可能存在一项名为ConstantValue的属性,值指向常量123.

字段表不会列出从父类或接口中继承来的字段,但有可能列出原来java代码中不存在的字段。

方法表集合

方法表结构和字段几乎一样。

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

方法访问标志有所差异

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 是否是编译器产生的桥接方法
ACC_VARARGS 0x0080 是否接受可变参数
ACC_NATIVE 0x0100 Declared native; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

方法经过编译后,方法内的代码存放在一个名为Code的属性里面,属性表是Class文件格式最具有扩展性的一种数据项目。

JVM学习--Class类文件结构
00 02 代表两个方法,一个方法是编译器添加的实例构造器,另一个是源码中的inc()。第一个方法的访问标识值为00 01就是public,方法名为00 07:描述索引值为00 08对应常量为()V,属性表计数器为00 01代表有一个属性 再查看00 09发现为Code,说明该属性是方法的字节码描述。

如果父类方法在子类中没有被重写,方法表中就不会出现父类的方法信息。但是有可能出现编译器自动添加的方法,最典型的是类构造器和方法。

属性表集合

属性表限制比较宽松,而且可以自定义,篇幅超级无敌长,这里就随便拿个。。Code属性的

Code_attribute {
    u2 attribute_name_index;  属性名称
    u4 attribute_length;   属性值的长度(整个属性表 - 6),6是attribute_name_index+ 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];
}

如果字节码从第start_pc行到第end_pc行之间出现了类型为 catch_type或其子类的异常,则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到 handler_pc进行处理。

参考

周志明著《深入理解java虚拟机》

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html