深入理解JVMClass类文件的结构

编程

Class文件本质

Class文件本质上是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

Class文件格式

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。

文件格式

类型

名称

数量

描述

u4

magic

1

魔数(魔法数字),表明当前文件是.class文件,固定0xCAFEBABE

u2

minor_version

1

副版本号

u2

major_version

1

主版本号,从45开始

u2

constant_pool_count

1

常量池计数值

cp_info

constant_pool

constant_pool_count - 1

常量池内容

u2

access_flags

1

类访问标识

u2

this_class

1

当前类

u2

super_class

1

父类

u2

interfaces_count

1

实现的接口数

u2

interfaces

interfaces_count

实现接口信息

u2

fields_count

1

字段数量

field_info

fields

fields_count

包含的字段信息

u2

methods_count

1

方法数量

method_info

methods

methods_count

包含的方法信息

u2

attributes_count

1

属性数量

attribute_info

attributes

attributes_count

各种属性

示例

public class ClassFormat {

private int m;

public int inc() {

return m + 1;

}

}

直接查看Class文件:

我们可以使用javap -verbose ClassFormat直接翻译Class文件,而不用人工翻译,如:

PS E:> javap -verbose ClassFormat

警告: 二进制文件ClassFormat包含com.xiaolyuh.ClassFormat

Classfile /E:/ClassFormat.class

Last modified 2020-1-18; size 385 bytes

MD5 checksum 6fe0cc9a0e7bf5b27718cb7e42ff6bfd

Compiled from "ClassFormat.java"

public class com.xiaolyuh.ClassFormat

minor version: 0 // 次版本号

major version: 52 // 主版本号

flags: ACC_PUBLIC, ACC_SUPER // 类的访问标志

Constant pool: // 常量池

#1 = Methodref #4.#18 // java/lang/Object."<init>":()V [从1开始]

#2 = Fieldref #3.#19 // com/xiaolyuh/ClassFormat.m:I

#3 = Class #20 // com/xiaolyuh/ClassFormat [类索引(this_class)表示引用20索引位]

#4 = Class #21 // java/lang/Object [父类索引(super_class)]

#5 = Utf8 m // 成员变量m的简单名称

#6 = Utf8 I // 表示int类型

#7 = Utf8 <init> // 实例构造器

#8 = Utf8 ()V // 没有参数,没有返回值的方法

#9 = Utf8 Code

#10 = Utf8 LineNumberTable

#11 = Utf8 LocalVariableTable

#12 = Utf8 this

#13 = Utf8 Lcom/xiaolyuh/ClassFormat;

#14 = Utf8 inc // 方法inc的简单名称

#15 = Utf8 ()I

#16 = Utf8 SourceFile

#17 = Utf8 ClassFormat.java

#18 = NameAndType #7:#8 // "<init>":()V

#19 = NameAndType #5:#6 // m:I

#20 = Utf8 com/xiaolyuh/ClassFormat

#21 = Utf8 java/lang/Object

{

public com.xiaolyuh.ClassFormat(); // 构造函数

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1 // args_size=1的原因是this变量

0: aload_0 // 将局部变量槽 0(即this指针)的元素入栈

1: invokespecial #1 // Method java/lang/Object."<init>":()V

4: return // 方法正常返回

LineNumberTable:

line 7: 0

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lcom/xiaolyuh/ClassFormat;

public int inc();

descriptor: ()I

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: getfield #2 // Field m:I

4: iconst_1

5: iadd

6: ireturn

LineNumberTable:

line 11: 0

LocalVariableTable:

Start Length Slot Name Signature

0 7 0 this Lcom/xiaolyuh/ClassFormat;

}

SourceFile: "ClassFormat.java"

更多指令请参考深入理解JVM - 虚拟机字节码指令集。

常量池

常量池中的第一个常量有特殊含义,所以常量池计数是从1开是的,其他计数是从0开始,如:接口索引集合。如上图,常量池计数是16,表示有22个常量,排除第一个,表示有21个常量,索引值范围为1~21。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

  • 字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
  • 符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

访问标志

用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

字段访问标志:

描述符

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。

描述符表:

java类型 | 类型描述符

---|---

boolean|Z

char |C

byte|B

short|S

int|I

long|J

float| F

double |D

Object |Ljava/lang/Object;

int[]| [I

Object[][]| [[Ljava/lang/Object;

方法描述符:

方法|方法描述符

---|---

void m(int i, float f)|(IF)V

int m(Object o)|(Ljava/lang/Object;)I

int[] m(int i, String s)|(ILjava/lang/String;)I

Object m(int[] i)|([I)Ljava/lang/Object;

方法表集合

描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。

与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”。

属性表集合

存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。

Code属性表的结构:

  • max_stack代表了操作数栈(Operand Stack)深度的最大值。
  • max_locals代表了局部变量表所需的存储空间。max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
  • code_length和code用来存储Java源程序编译后生成的字节码指令。

参考

《深入理解JAVA虚拟机》

以上是 深入理解JVMClass类文件的结构 的全部内容, 来源链接: utcz.com/z/512873.html

回到顶部