JVM 之 Class文件结构本文写作目的: 1)为了加深自己学习的理解,2)帮助正在学习研究JVM的同仁,3)与任何热爱技术的达人交流经验,提升自己 以此为本,文章会尽量写的简洁,尽量保证理解的正确性,如有任何理解不到位或错误的地方,希望朋友们及时指出,严厉拍砖。 开始之前我们需要先了解一些基本的概念,这些概念是学习整个JVM原理的基础。 1)JVM虚拟机规范主要规范了Class文件结构,虚拟机内存结构,虚拟机加载,解析,执行Class文件的行为方式,以及一系列的字节码指令集。 2)Class文件理论上说是一种数据结构,该数据结构有着严格的格式规范,该规范在字节粒度上规定了组成该数据结构的格式标准。 3)Class文件本质上是一组二进制字节流,是被JVM解析执行的数据源,每个字节都有着不同的含义,可能表示字符,数字,也可能表示执行某种操作的一个字节码指令。 4)JVM (Java 虚拟机)是解析执行Class文件的核心引擎,是整个Java系统的运行时环境,是跨平台的基石。 5)我们的Java代码需要被编译器编译成完整,正确的Class文件才能被JVM正确的执行。 6)编译器并非JVM的一部分,不同的语言可以提供不同的编译器,其作用是将该语言的代码编译为正确的Class文件,如Scala,JRuby等等。 7)JVM是完全开放的跨平台的,只要你有能力你可以按照JVM虚拟机规范编写自己的编程语言。 8)JVM 使得Java的跨平台成为可能,那么Class文件结构规范则使得更多的编程语言运行在JVM上成为可能。 既然Class文件是一种数据结构,那么到底是什么样的数据结构呢?通常计算机中的文件都包含元数据和实体数据两部分,元数据用来存储该文件的描述信息,实体数据来存放用于表达文件真实内容的数据。当然Class文件也不例外,我们为了便于理解,也将class文件的结构分为两大部分:元数据和实体数据(注:非规范定义,只是为了方便理解进行的自定义)。 元数据:包含了Class文件的魔术数(标识符)和版本信息。 实体数据:包含了常量池,字段描述,方法描述,属性描述等用于表述该类行为的具体信息。 元数据我们不多赘述对我们后面的分析没多大关系,下面主要分析下实体数据。 一,结构概览不管是元数据还是实体数据他们都以字节为单位按顺序紧凑的排列在class文件中,没有任何多余空间。为了描述class文件结构,虚拟机规范定义了u1, u2, u4, u8四种基本数据结构和一种由这四种基本数据结构组成的复杂数据结构-表(通常以info结尾表示),这四种基本数据结构分别表示一个字节,两个字节,四个字节,八个字节。基于此我们便可以清晰的了解class文件结构的总体轮廓了(C语言语法,其中常量表,变量表,方法表,属性表都有一到多个,因此定义为数组),如<<代码一>>
注:表本身是一种复杂结构,包含多个字节,Class文件机构中定义了四种表结构,1)常量表2)变量表3)方法表4)属性表。由于每种表都有一到多个,所以在<<代码一>>中可以看出他们都是在数组中的。 接下来我们按顺序研究下每个部分的具体含义! 二,常量池Class文件中的常量池大小(constant_pool_count)由第九,第十两个字节(前八个字节用来描述版本信息)决定,我们知道两个字节最大可以表示65535,这也就表明一个Class文件中最多可以具备65535个常量,包括数值,字符串,类名,方法名,变量名等等。接下来的constant_pool_count个字节就用来描述所有的常量了。为了能表示各种可能类型的值,常量在Class文件中被定义成一种复杂结构:如<<代码二>> 可以看出,常量的第一个字节表示了该常量的类型。 注:constant_pool_count的值为常量个数+1,并且常量池常量的索引从1开始!!! 下面为各类型的映射表:<<表一>>
由上表可以看出,目前为止JVM 一共定义了14种类型的常量。 每个常量表的第一个字节表明了常量的类型,那么剩余的值则根据类型的不同表明了不同的含义,可能是一个直接值,也可能是一个对另一个常量的引用,那么不同类型的常量表定义如下: <<代码三>> Constant_Utf8_info常量用来表示一个utf8字符串,其长度为可变长度,第一个字节的值固定为1(<<表一>>中Constant_Utf8(1)),后面两个字节表示了个字符串的字节长度length(而不是字符串的长度),然后后面紧跟着的length个字节就是字符串的字节码了。该常量被引用的频率颇高,如类名,方法名等常量都引用它。 <<代码四>> Constant_Class_info常量用来表示一个类或者接口,一共包含三个字节,第一个字节的值固定为 7(<<表一>>中Constant_Class(7)),后两个字节的值为对常量池中另一个常量(Constant_Utf8_info)的索引,该Constant_Utf8_info常量的值应为JVM的内部二进制类或接口名(binary class or interface name下文详解)。 <<代码五>>
代码五中的三个非常相似的常量结构分别表示了变量,方法,接口方法,其中各个值得含义和大小已在注释中说明。其实很容易理解,比如一个方法,它属于哪个类(class_index),它的名字和类型(name_and_type)。 <<代码六>> Constant_String_info表示了一个String类型的常量实例。一共占用三个字节,第一个字节固定为8,后两个字节为对Constant_Utf8_info常量的索引。 <<代码七>>
上面四个常量结构分别表示int, float, long,double类型的常量,比较直观,不多赘述。 <<代码八>> Constant_NameAndType_info用来表示变量或者方法的名字和类型的常量,该常量不包含该变量或方法的所属类或接口的引用,主要用来被Constant_Method_info,Constant_Field_info等常量使用。该常量一共包含五个字节,第一个固定为12,第二三个的值为对Constant_Utf8_info常量的引用,该Constant_Utf8_info常量的值应为变量或方法的有效的非限定名( unqualified name 下文详解),第四五的值为对Constant_Utf8_info常量的引用,该Constant_Utf8_info常量的值应为有效的变量或方法的描述符(field descriptor,method descriptor下文详解)。 <<代码九>> Constant_MethodHandle_info常量用来表示方法句柄,第一个字节固定为15,第二个字节的值为0-9,分别表明了该句柄的不同字节码行为,其值的描述见<<表二>>,最后两个字节为对常量池中某常量的引用,但具体引用那种常量由reference_kind而定。 a,如果reference_kind的值为1 ( b,如果reference_kind的值为5 ( c,如果reference_kind 的值是9(REF_invokeInterface)时,reference_index的值必须为对Constant_InterfaceMethodref_info常量的引用,表示该句柄所用与的接口方法。 d,如果reference_kind 的值是5 ( e,如果reference_kind的值是8 ( <<表二>>
<<代码十>> Constant_MethodType_info常量用来表示方法类型的常量,描述很直观,不多赘述。 <<代码十一>> Constant_InvokeDynamic_info常量用来指定invokedynamic指令的引导方法,动态调用名称,调用的参数和返回值hi,以及一些列可选的引导方法使用的叫做静态参数的常量。 三,访问标志位Class文件中紧跟在常量池后的访问标志位,一共占用两个字节,也就是十六个bit位,每个bit位标记一种类的访问修饰符,如final,abstract,public等,现在JVM已经使用了其中的八个,其余八个保留位未来使用,并且必须置零。八个标志位映射如下表<<表三>>
各个标志位间有一定的约束条件,如ACC_ANNOTATION置位时,ACC_INTERFACE 必须置位等。 四,类/父类/接口的描述Class文件中紧跟在访问标志位后的是this_class, super_class, interface_count, interfaces[],分别用来表示该类,该类的直接父类(是直接父类哦),实现的接口数量,以及接口信息等。 A,其中this_class用来表示该类的信息,其值为对常量池中Constant_Class_info常量的引用。 B,super_class用来表示该类的直接父类父类信息,其值要么是0要么是对常量池中Constant_Class_info常量的引用。但有以下几点需要注意: 1)如果其值为对常量池Constant_Class_info 的引用,那么被引用的类(直接父类)的ACC_FINAL访问标志位必须不能被置位。 2)如果其值为0,那么该类必须,一定是Object类 3)接口的super_class值必须是对常量池Constant_Class_info常量的引用,并且该常量表示的是Object类。 C,interface_count表明了该类实现接口的数量,而interfaces[]表,则表明了所有的实现接口。其中每一个interface的值占用两个字节,总共占用interface_count * 2个字节,都是对常量池Constant_Class_info 常量的引用。 五,变量表(字段表)
接下来紧跟在接口定义后面的是变量个数和变量表。该表结构用来描述类中的某个变量定义,不会同时有两个名字和描述符都相同的变量。变量的结构描述结构如下<<代码十二>> 上面的结构便是Class文件中用来描述某个变量(实例变量,类变量等)的定义。前三个u2字节分别表明了变量的访问修饰符,名称和描述符(下文详解)。其中access_flag 映射如下表<<表四>>
名称和描述符都是对常量池中Constant_Utf8_info常量的索引。以上三个U2可以描述一个没有初始值的变量定义了,如private static int i; 但是如果指定了private static int i = 1;那么则会用到名为ConstantValue的attribute_info结构,下文讲解attribute_info时详解。变量中的属性除了ConstantValue外还可能含有 六,方法表
Class文件中跟在变量表后面的是方法个数和方法表,该表结构表示一个方法的定义,其中也会包括实例初始化方法(instance initialization method, <init>),类和接口初始化方法(class or interface initialization method, <clinit>)。方法表的描述如下<<代码十三>> 该结构跟变量表的结构几乎完全相同,包括方法的访问标志位,名称索引,描述符索引等。下表为方法的访问标志位映射 <<表五>>
不同的是,方法中包含的属性种类跟变量中的属性种类有所不同,其中可能包含
|
Attribute | Java SE | class file |
|
---|---|---|---|
ConstantValue |
1.0.2 | 45.3 | |
Code |
1.0.2 | 45.3 | |
StackMapTable |
6 | 50.0 | |
Exceptions |
1.0.2 | 45.3 | |
InnerClasses |
1.1 | 45.3 | |
EnclosingMethod |
5.0 | 49.0 | |
Synthetic |
1.1 | 45.3 | |
Signature |
5.0 | 49.0 | |
SourceFile |
1.0.2 | 45.3 | |
SourceDebugExtension |
5.0 | 49.0 | |
LineNumberTable |
1.0.2 | 45.3 | |
LocalVariableTable |
1.0.2 | 45.3 | |
LocalVariableTypeTable |
5.0 | 49.0 | |
Deprecated |
1.1 | 45.3 | |
RuntimeVisibleAnnotations |
5.0 | 49.0 | |
RuntimeInvisibleAnnotations |
5.0 | 49.0 | |
RuntimeVisibleParameterAnnotations |
5.0 | 49.0 | |
RuntimeInvisibleParameterAnnotations |
5.0 | 49.0 | |
AnnotationDefault |
5.0 | 49.0 | |
BootstrapMethods |
7 | 51.0 |
上表可以看出每种属性的名字和初始版本信息,Java SE7中新加入了BootstrapMethods属性,invokedynamic指令等实现动态语言的特性。
限于篇幅,我们这里只分析Code,ContantValue属性。
<<代码十五>>定义了ContantValue属性表的结构
可以看出该属性是定长的表结构,总共有8个字节大小。前面讲过了前两部分用来表明属性的名字和大小,ContantValue属性表中的name-index索引的常量值固定为“ContantValue”。另外该属性只会出现在变量表(field_info)中,用来表示该变量的值。
constantvalue_index也是对常量池中某常量的索引,其索引的常量类型根据变量的类型不同而不同,如下表<<表七>>
Field Type | Entry Type |
---|---|
long |
CONSTANT_Long |
float |
CONSTANT_Float |
double |
CONSTANT_Double |
int , short , char , byte , boolean |
CONSTANT_Integer |
String |
CONSTANT_String |
相比ConstantValue属性,Code属性相对复杂些,其结构定义如下<<代码十六>>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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]; } |
前两部分与ConstantValue属性表一样,表示名字索引和大小,不同的是被索引的名字必须为“Code”。Code属性只可以出现在方法表(method_info)中,但是如果一个方法为abstract或者native的,那么其方法表不可以包含Code属性表。否则必须有且只有一个属性表。
1) max_stack:表明方法执行的任意时刻该方法操作数栈的最大深度。
2) max_locals:表明方法执行的任意时刻该方法的本地变量表中变量的最多个数。
关于操作数栈,本地变量表等运行时内存的相关知识,下篇文章深入分析。
3) code_length:顾名思义,表明了方法体的字节码大小。
4) code[code_length]:这里便是所有方法体字节码的真正所在地了!!JVM规范对这块有很长篇幅的约束,如长度大于0小于65535等等已超出本文范围,不做深究,感兴趣可以查看http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.9
接下来的两个字节exception_table_length,定义了方法中异常表的个数。异常表的结构JVM没有单独定义,而是直接定义在了Code属性表中,每个异常表表示一个异常处理块,从上面代码可以看出,每个异常表有4个U2字节:
1) start_pc:异常处理块的开始字节码索引(code[code_length]中)取值范围是[0, end_pc).
2) end_pc:异常处理块的结束字节码索引(code[code_length]中)取值范围是(start_pc, code_length).
这里有个有趣的地方,start_pic被定义为inclusive的,就是可以包含第start_pc个字节码, 而end_pc被定义为exclusive的,是不包含第end_pc个字节码的,这样就有一个问题,如果代码长度为65535,并且end_pc也是65535那么最后一个字节码指令就无法被异常处理块捕获处理。这是JVM设计中的一个BUG,规范中已经指出。(严谨程度可见一斑)
3) handler_pc:异常处理代码的字节码索引(code[code_length]中)。
5) catch_type:捕获异常的类型,常量池中constant_class_info常量的索引,如果是0则捕获所有异常。
异常表后面是另一个属性表的信息了。在Code属性表中的属性表(可见属性表的灵活性了吧)可以是LineNumberTable
,LocalVariableTable
,LocalVariableTypeTable
,and StackMapTable
中的一个或多个,主要提供IDE调试功能用的。这里我们就不再分析。
到此为止我们整个Class文件结构已经分析的差不多了,相信如果你从头认真阅读后会有很大收获的。但是我们上面还有一个问题没有弄明白就是binary class or interface name, unqualified name,descriptor有什么区别和意义。
1)binary class or interface name,在Class文件中一个类或接口的名字通常都是全限定名(包名+类名),这就称作binary names。如java.lang.Thread。但是由于当年ASCII中点号(.)常被用来表示某些特用意义,因此Java中用斜杠(/)来代替了它,就变成了java/lang/Thread。这就是binary class。
2)unqualified name,Class文件中变量,方法的名字以非限定名的形式保存的,简单讲就是单纯的变量名或方法名,是不能包含./[;等ASCII字符的。但有个例外<init><clinit>,前者是实例初始化方法,后者是类初始化方法。
3)descriptor,用来描述变量或方法类型的字符串。即用一个或多个简单的字符来表达Java中的不同类型,其对应表如下<<表八>>
BaseType Character | Type | Interpretation |
---|---|---|
B |
byte |
signed byte |
C |
char |
Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16 |
D |
double |
double-precision floating-point value |
F |
float |
single-precision floating-point value |
I |
int |
integer |
J |
long |
long integer |
L ClassName ; |
reference |
an instance of class ClassName |
S |
short |
signed short |
Z |
boolean |
true or false |
[ |
reference |
one array dimension |
对于一个int类型的变量其descriptor就是 I
对于一个Object类型的变量其descriptor就是Ljava/lang/Object.
对于一个double[][]变量其descriptor就是[[[D
不过对于方法来说稍微复杂点,descriptor是按“(参数列表)返回值”的顺序描述的
如Object m(int i, double d, Thread t){}方法定义的descriptor就是(IDLjava/lang/Thread)Ljava/lang/Object。
好了到此我们的Class文件结构理论已经完全分析完成,不过没有例子检验下我们的分析会不会太过分了。
九,实例分析
废话不说上实例,我们从字节码一个一个的来推源码,走起。。。上字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 0000000 : cafe babe 0000 0034 0016 0a00 0500 1209 ....... 4 ........ 0000010 : 0003 0013 0700 140a 0003 0012 0700 1501 ................ 0000020 : 0003 6f62 6a01 0012 4c6a 6176 612f 6c61 ..obj...Ljava/la 0000030 : 6e67 2f4f 626a 6563 743b 0100 0669 7661 ng/Object;...iva 0000040 : 6c75 6501 0001 4901 0006 3c69 6e69 743e lue...I...<init> 0000050 : 0100 0328 2956 0100 0443 6f64 6501 000f ...()V...Code... 0000060 : 4c69 6e65 4e75 6d62 6572 5461 626c 6501 LineNumberTable. 0000070 : 0004 6d61 696e 0100 1628 5b4c 6a61 7661 ..main...([Ljava 0000080 : 2f6c 616e 672f 5374 7269 6e67 3b29 5601 /lang/String;)V. 0000090 : 000a 536f 7572 6365 4669 6c65 0100 0d4a ..SourceFile...J 00000a0: 766d 436c 6173 732e 6a61 7661 0c00 0a00 vmClass.java.... 00000b0: 0b0c 0008 0009 0100 084a 766d 436c 6173 .........JvmClas 00000c0: 7301 0010 6a61 7661 2f6c 616e 672f 4f62 s...java/lang/Ob 00000d0: 6a65 6374 0021 0003 0005 0000 0002 0002 ject.!.......... 00000e0: 0006 0007 0000 0002 0008 0009 0000 0002 ................ 00000f0: 0001 000a 000b 0001 000c 0000 0027 0002 .............'.. 0000100 : 0001 0000 000b 2ab7 0001 2a10 17b5 0002 ......*...*..... 0000110 : b100 0000 0100 0d00 0000 0a00 0200 0000 ................ 0000120 : 0100 0400 0300 0900 0e00 0f00 0100 0c00 ................ 0000130 : 0000 3400 0300 0200 0000 14bb 0003 59b7 .. 4 ...........Y. 0000140 : 0004 4c2b 59b4 0002 102d 60b5 0002 b100 ..L+Y....-`..... 0000150 : 0000 0100 0d00 0000 0e00 0300 0000 0600 ................ 0000160 : 0800 0700 1300 0800 0100 1000 0000 0200 ................ 0000170 : 110a .. |
上面的代码为某个类的Class文件的十六进制形式。每行有16个字节。好了我们按照规则解读,前八个字节跳过,从第九个字节开始的两个字节是常量池大小, 22,说明有21个常量(原因见上文)。为了节省版面我们只分析前两个常量池中的常量及其值:
我们知道常量表的第一个字节表明了常量的类型,所以前两个常量表的类型分别是10(constant_method_ref)和9(constant_field_ref)。类型断定那么该常量表的结构也就都断定了,后面的两个字节是对class常量的索引,索引值分别为5和3,也就是第5和第3个常量。再后面两个字节是对NameAndType常量的索引,索引值分别为18和19,也就是第18和19两个常量。然后在查找3,5,18,19个常量以此类推就可以推出所有常量的值了。我们不多赘述。
常量池完了是this_class, super_class, interfaces我们跳过,直奔field_info,我已经计算出了field_info的起始位置,直接列出:
我们知道field_info前的两个字节是变量个数,0002也就是2个变量,接下来就进入第一个变量表了,变量表的前两个字节是访问符,0002,对照<<表四>>可知是private,接下来的两个U2字节分别是名字常量索引和描述符常量索引,索引值分别是6和7,查找常量池发现分别是obj和Ljava/lang/Object,再接下来的两个字节是属性个数,0000说明没有属性。这样第一个变量我们就知道它的定义如下:
同样分析第二个变量:
变量表分析完了下面是方法表:
前两个字节还是方法个数,0002,说明两个方法,接下来进入方法表,方法表前两个字节是方法的访问修饰符,0001查看<<表五>>可知是public的。接下来的两个U2字节分别是名字常量索引和描述符常量索引,索引值分别是10(000a)和11(000b),查找常量池发现分别是<init>和()V。接着后面两个字节是属性表个数,0001,说明有一个属性,进入属性表,前两个字节是属性名的索引,000c查看常量池是“Code”,说明是Code属性,Code属性表的3,4,5,6四个字节是属性表的字节大小,0000 0027=39。说明有39个字节。然后是0002(2)最大栈深,0001(1)最大本地表量表,以及0000 000b=11,表明有11个字节码是真正的方法体。
我们先不理会方法体的内容,最后的0000表明该方法没有异常处理块。
到此我们可大致写出该方法的定义了
这个方法就是实例初始化方法,也就是默认无参构造器。( 以上分析只是为了了解Class文件结构,JDK提供了一个分析字节码的工具javap,可以快速简单的分析字节码)
好了到此为止所有分析就算完成了。希望你会有所收获。
http://docs.oracle.com/javase/specs/jvms/se7/html/ (The Java Virtual Mathine Specification, Java SE 7 Edition.)
|
来自: 昵称21069626 > 《待分类1》