一直不太搞得明白jvm到底是如何进行类加载的,在看资料的过程中迷迷糊糊,在理解类加载之前,首先看看java的类文件结构到底是怎样的,都包含了哪些内容。
最直接的参考当然是官方文档:The Java® Virtual Machine Specification
我写了一个最简单的java程序,根据这个程序来分析一下.class文件中到底都存了些什么。
java程序:
class Par {
public int x = 5;
public void f(){
System.out.println("par static");
}
}
class Sub extends Par{
public int x = 6;
public void f() {
System.out.println("Sub static");
}
}
public class Test {
public static void main(String[] args) {
Par par = new Sub();
System.out.println(par.x);
par.f();
}
}
生成的.class文件内容
cafe babe 0000 0033 0027 0a00 0900 1207
0013 0a00 0200 1209 0014 0015 0900 1600
170a 0018 0019 0a00 1600 1a07 001b 0700
1c01 0006 3c69 6e69 743e 0100 0328 2956
0100 0443 6f64 6501 000f 4c69 6e65 4e75
6d62 6572 5461 626c 6501 0004 6d61 696e
0100 1628 5b4c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5601 000a 536f 7572
6365 4669 6c65 0100 0954 6573 742e 6a61
7661 0c00 0a00 0b01 0016 636f 6d2f 7468
696e 6b69 6e67 696e 6a61 7661 2f53 7562
0700 1d0c 001e 001f 0700 200c 0021 0022
0700 230c 0024 0025 0c00 2600 0b01 0017
636f 6d2f 7468 696e 6b69 6e67 696e 6a61
7661 2f54 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 106a 6176
612f 6c61 6e67 2f53 7973 7465 6d01 0003
6f75 7401 0015 4c6a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 3b01 0016 636f
6d2f 7468 696e 6b69 6e67 696e 6a61 7661
2f50 6172 0100 0178 0100 0149 0100 136a
6176 612f 696f 2f50 7269 6e74 5374 7265
616d 0100 0770 7269 6e74 6c6e 0100 0428
4929 5601 0001 6600 2100 0800 0900 0000
0000 0200 0100 0a00 0b00 0100 0c00 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000d 0000 0006 0001 0000 0018 0009
000e 000f 0001 000c 0000 003b 0002 0002
0000 0017 bb00 0259 b700 034c b200 042b
b400 05b6 0006 2bb6 0007 b100 0000 0100
0d00 0000 1200 0400 0000 1b00 0800 1c00
1200 1d00 1600 1e00 0100 1000 0000 0200
11
使用javap反汇编一下生成的.class文件(javap -v com.thinkinginjava.Test)
Classfile /E:/Workspaces/MyEclipse/websocketTest/src/com/thinkinginjava/Test.cla
ss
Last modified 2016-4-2; size 513 bytes
MD5 checksum 32ca9f73cf634398be1085454c6b21c3
Compiled from "Test.java"
public class com.thinkinginjava.Test
SourceFile: "Test.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // com/thinkinginjava/Sub
#3 = Methodref #2.#18 // com/thinkinginjava/Sub."<init>":()V
#4 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Fieldref #22.#23 // com/thinkinginjava/Par.x:I
#6 = Methodref #24.#25 // java/io/PrintStream.println:(I)V
#7 = Methodref #22.#26 // com/thinkinginjava/Par.f:()V
#8 = Class #27 // com/thinkinginjava/Test
#9 = Class #28 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 com/thinkinginjava/Sub
#20 = Class #29 // java/lang/System
#21 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#22 = Class #32 // com/thinkinginjava/Par
#23 = NameAndType #33:#34 // x:I
#24 = Class #35 // java/io/PrintStream
#25 = NameAndType #36:#37 // println:(I)V
#26 = NameAndType #38:#11 // f:()V
#27 = Utf8 com/thinkinginjava/Test
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 com/thinkinginjava/Par
#33 = Utf8 x
#34 = Utf8 I
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
#38 = Utf8 f
{
public com.thinkinginjava.Test();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 24: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/thinkinginjava/Sub
3: dup
4: invokespecial #3 // Method com/thinkinginjava/Sub."<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: getfield #5 // Field com/thinkinginjava/Par.x:I
15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
18: aload_1
19: invokevirtual #7 // Method com/thinkinginjava/Par.f:()V
22: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 18
line 30: 22
}
根据java虚拟机规范,首先介绍一下Class 文件格式
“Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。”
无符号数属于基本的数据结构,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数;
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都习惯性的以“_info”结尾。
Class文件结构
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];
}
一个个来分析:
魔数、主次版本号
我本地的是JDK 1.7.0的,所以是主次版本号是 00 00 00 33,不同的jdk版本生成的版本号不同。
常量池
在主次版本号后面的就是常量池入口,由于常量池的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值
可见当前的计数值为0x0027 ,即39,这代表常量池中有38项常量,索引号为1-38,Class文件只有常量池的容器技术是从1开始的
从之前的反汇编代码中也可以看到,常量池中确实有38项
常量池中主要有3类常量:
- 类和接口的全限定名
- 字段的名称和描述符
-
方法的名称和描述符
常量池中的项目类型如下:
每一种常量都有它的结构,例如:
CONSTANT_Class_info {
u1 tag; //标志,占1个字节 07
u2 name_index;//指向全限定名常量项的索引
}
CONSTANT_Utf8_info {
u1 tag;//标志 01
u2 length;//UTF-8编码的字符串占用的字节数
u1 bytes[length];//长度为length的UTF-8编码的字符串
}
。。。。每一项可具体参考官方文档
举几个例子:
以#1为例, #1 = Methodref #9.#18
The CONSTANT_MethodHandle_info Structure
CONSTANT_MethodHandle_info {
u1 tag; //标志 10
u1 reference_kind; //指向声明方法的类描述符的CONSTANT_Class_info的索引项
u2 reference_index;//指向名称及类型描述符CONSTANT_NameAndType的索引项
}
tag:0a
reference_kind:0x0009
reference_index:0x0012
以#10为例 #10 = Utf8 < init >
The CONSTANT_Utf8_info Structure
CONSTANT_Utf8_info {
u1 tag;//标志 01
u2 length;//UTF-8编码的字符串占用的字节数
u1 bytes[length];//长度为length的UTF-8编码的字符串
}
tag: 01
length: 6
bytes[length]:具体对应的编码 3c 69 6e 69 74 3e
可以看到,常量区总共有38项,在.class文件中的表示如下图所示,按照上面的分析即可分析出每个常量对应的是哪些16进制位
访问标记
分析完常量池,在常量池结束后,紧跟着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。
(直接截图了。。。)
从代码中可以分析出,我写的java程序的访问标志位0x0021,即ACC_PUBLIC、ACC_SUPER标志位真,其余标志为假,因此access_flags的值为0x0001 | 0x0020 = 0x0021(或运算即可算出是哪几个标志为真,哪几个为假),如下图所示。
类索引、父类索引与接口索引集合
类索引、父类索引和接口索引集合都按顺序排列在访问标记之后。
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合是一组u2类型的集合。
- 类索引用于确定这个类的全限定名
-
父类索引用于确定这个类的父类的全限定名
对于接口索引集合,入口第一项u2类型的数据为接口计数器(interface_count),表示索引表的容量。,没有实现任何接口,则计数为0,后面接口的索引表不再占用任何字节。
如下图所示,类索引对应0x0008,即com/thinkinginjava/Test类,父类索引为Object类,0x0009即指向java/lang/Object
字段表集合
字段表用于标书接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
java中描述一个字段包含的信息有:
- 字段的作用域(public,protected,private)
- 实例变量还是类变量(static)
- 可变性(final)
- 并发可见性(volatile)
- 可否序列化(transient)
- 字段数据类型(基本类型,对象,数组)
-
字段名称
关于字段表可以参考这篇博文看看
因为我的程序中Test类中没有定义字段,所以结果显示为0x0000,如下图
方法表集合
紧接着字段表的是方法表集合,理解了字段表看方法表就相对简单了
方法表的结构与字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项
官方文档
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法访问标志:
分析一下字节码:
- 方法表集合的计数器值为0x0002,代表集合中有两个方法(一个是编译去添加的实例构造器
<init> 和源码中的main方法) - 第一个方法的访问标志为0x0001,即只有ACC_PUBLIC标志为真
- 名称索引值为0x000a,查找常量池可得方法名为
<init> - 描述符索引值为0x000b,对应常量为”()V”
- 属性表计数器值为0x0001,表示有一个属性,对应的属性名称索引为0x000c,对应常量为“Code”,说明此属性时方法的字节码描述。
属性表集合
官方文档
属性表在之前出现过多次了,在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
对于每个属性,它的名称需要从一个常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
属性表结构如下所示:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
举几个例子:
1.Code属性
java方法体中的代码经过javac编译处理后,最终会变成字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果Code属性存在,那么它的结构如下所示:
Code_attribute {
u2 attribute_name_index; //指向CONSTANT_Utf8_info型常量的索引
u4 attribute_length; //属性值的长度
u2 max_stack; //操作数栈深度的最大值
u2 max_locals; //局部变量表所需的存储空间
//code_length和code用来存储java源程序编译后生成的字节码指令
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];
}
分析一下字节码:
- 由上面方法表的分析,第一个
<init> 方法对应的属性名为Code,其值为0x000c - 紧接着为attribute_length,表示属性值的长度,其值为0x0000001d
- 接着为max_stack,表示操作数栈深度的最大值,其值为0x0001
- 接着为max_locals, 表示局部变量表所需的存储空间,其值为0x0001
- 接着为code_length,占4个字节,表示字节码长度,其值为0x00000005
- 紧接着读入跟随的5个字节,根据字节码指令表翻译出对应的字节码指令。翻译“2a b7 00 01 b1”过程:
1)读入2a,对用指令aload_0
2)读入b7,对应指令invokespecial
3)读入00 01,这是invokespecial的参数,为常量池中0x0001对应的常量
4)读入b1,对应的指令为return
- 接着为exception_table_length,此处为0,没有异常
- 接着为attribute_count,代表这个Code中有1个属性
-
然后就是这个属性的描述啦,索引号为0x000d,找到对应的常量池索引号,为LineNumberTable,so,这个属性是LineNumberTable,那么看一下LineNumberTable属性结构:
LineNumberTable属性LineNumberTable_attribute {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u2 line_number_table_length;
{ u2 start_pc; //字节码行号
u2 line_number; //java源码行号
} line_number_table[line_number_table_length];
}从class文件中可以看出,
attribute_name_index为0x000d,
attribute_length为0x00000006,
line_number_table_length为0x0001,
start_pc为0x0001
line_number为0x0018
正好对应了LineNumberTable:
line 24: 0到此为止,一个
<init> 方法分析完了,包括了它包含的属性。
之前说了,该程序共有2个方法,还有一个为main函数,对它的分析跟上面一样,不做分析了
有些博客也可以参考一下,写的比较细
http://blog.csdn.net/u010349169/article/category/2620885