jvm之class文件及类加载机制和双亲委派模型
文章目录
本文先简单介绍虚拟机规范约定的class
文件格式,并从一个示例class
文件复原代码基本内容,最后描述虚拟机类加载机制以及介绍一下双亲委派模型。Java版本为11。
1 class文件格式
class
文件是字节码文件,文件内容基本单位就是一个字节,文件中不同的项根据约定的格式用一个或多个字节表示。
下面是class
文件的基本结构:
|
|
在分别介绍上面的每一个条目之前,先说明几点:
- “u4”,“u2"分别表示4个字节和2个字节
- 每一项都是严格按照顺序排列的。
- cp_info,field_info,method_info,attribute_info,这几个没有标明长度,因为他们对应的条目是复杂结构,其长度根据不同的条目会变化(但是可以从条目信息中获取相应的长度),整个
class
文件类似一个目录结构
下面按顺序解释每一个条目:
magic
4个字节。其值固定是0xCAFEBABE
,这是一个16进制的值,名字起源可参考wiki
minor_version
2个字节。class
文件次版本号,现在版本大都为0,以前
major_version
2个字节。class
文件主版本号,比如java11对应的55,虚拟机都是backward compatibility,所以可以支持旧版的class
文件,对应的版本支持如下:
Java SE | class file format version range |
---|---|
1.0.2 | 45.0 ≤ v ≤ 45.3 |
1.1 | 45.0 ≤ v ≤ 45.65535 |
1.2 | 45.0 ≤ v ≤ 46.0 |
1.3 | 45.0 ≤ v ≤ 47.0 |
1.4 | 45.0 ≤ v ≤ 48.0 |
5.0 | 45.0 ≤ v ≤ 49.0 |
6 | 45.0 ≤ v ≤ 50.0 |
7 | 45.0 ≤ v ≤ 51.0 |
8 | 45.0 ≤ v ≤ 52.0 |
9 | 45.0 ≤ v ≤ 53.0 |
10 | 45.0 ≤ v ≤ 54.0 |
11 | 45.0 ≤ v ≤ 55.0 |
constant_pool_count
2个字节。常量池条目的数量,值是“实际条目数量+1”,一个说明是因为计算的时候把constant_pool_count
本身也算在内了,其下标是0。
constant_pool[constant_pool_count-1]
常量池,对应的条目下标编号是从1开始,直到constant_pool_count-1
,每一个条目的基本结构如下:
|
|
tag
表是这个条目是什么类型的,info[]
表示后面的具体信息。
比如,tag
是1表示CONSTANT_Utf8
,对应的结构如下:
|
|
注意,这里的tag和上面tag是同一个,也就是说,length和bytes[length]替换了info[]
的位置。
全部的常量池类型参考虚拟机规范
access_flags
2个字节。类型的一些信息,可以是几个值的组合,每个FLAG信息如下:
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
ACC_FINAL | 0x0010 | Declared final; no subclasses allowed. |
ACC_SUPER | 0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. |
ACC_INTERFACE | 0x0200 | Is an interface, not a class. |
ACC_ABSTRACT | 0x0400 | Declared abstract; must not be instantiated. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_ANNOTATION | 0x2000 | Declared as an annotation type. |
ACC_ENUM | 0x4000 | Declared as an enum type. |
ACC_MODULE | 0x8000 | Is a module, not a class or interface. |
上面的value用的是16进制表示,转化成2进制可以发现这些值都是1个16位的二进制数在不同的位取1,其它15位都是0,这种方法可以非常方便的用二进制操作来判断对应的FLAG是否存在,比如我们的FLAGS值是:
0010 0010 0000 0000
,用ACC_INTERFACE
的值0000 0010 0000 0000
去跟FLAGS的值做与运算,返回不是0的值,就表示ACC_INTERFACE
这个FLAG存在。不同的FLAG对应的值就是位掩码(mask)。
this_class
2个字节。指向常量池中一个条目,被指向的条目必须是一个CONSTANT_Class_info
类型。
super_class
2个字节。通常是一个非0值指向常量池中一个条目,被指向的条目必须是一个CONSTANT_Class_info
类型。只有Object
这个类本身的class
文件,这个值是0。
interfaces_count
2个字节。表示当前class
直接实现或者继承(对于接口class)的接口数量。
interfaces[interfaces_count]
2个字节为单位。每个值指向常量池内的一个条目,被指向的条目必须是一个CONSTANT_Class_info
类型。
fields_count
2个字节。表示field
的个数,field
包括类的字段和实例的字段。
fields[fields_count]
field
集合。单个field
的基本结构如下:
|
|
- access_flags:跟前面描述的access_flags区别就是
class
和field
可以匹配的FLAG会有差别,比如ACC_PROTECTED
(0x0004)在class
中就没有。具体参考虚拟机规范。 - name_index:指向常量池的条目,被指向的条目类型必须是
CONSTANT_Utf8_info
,其值必须是一个有效的非全限定名(关于名字中不能使用一些特殊字符,参考这里) - descriptor_index:指向常量池的条目,被指向的条目类型必须是
CONSTANT_Utf8_info
,其值是一个有效的field descriptor
,内容表示field
的type,分下列3种:- BaseType:对应8大原始类型,
B,C,D,F,I,J,S,Z
分别表示byte,char,double,float,int,long,short,boolean
,除了long,boolean
,其它的都是单词的首字母 - ObjectType:对象类型,字母
L
开头,;
结尾,格式:LClassName;
如Ljava/lang/String;
- ArrayType:数组类型,
[
开头,格式[ComponentType
,如[Ljava/lang/String;
- BaseType:对应8大原始类型,
- attributes_count:属性的数量
- attributes[attributes_count]:属性的具体内容。属性这个条目可能在类、方法、字段这些条目中出现,我们放到最后类的属性时再讲。
methods_count
2个字节。表示method
的数量。
methods[methods_count]
method
集合。单个method
的基本结构如下:
|
|
method
和field基本结构是一样的,只说不同点:descriptor_index
所指向的这个条目的结构不同,这里指向的是method descriptor
,基本格式是( {ParameterDescriptor} ) ReturnDescriptor
:- {ParameterDescriptor}:表示0个-多个参数,类型是上面说的
field type
的3种类型之一,参数之间不用任何符号分隔 - ReturnDescriptor:表示返回值,取值可以是
field type
的3种类型之一或者V
(表示void)
举个例子:Object m(int i, double d, Thread t) {...}
的method descriptor
是:(IDLjava/lang/Thread;)Ljava/lang/Object;
- {ParameterDescriptor}:表示0个-多个参数,类型是上面说的
attributes_count
2个字节。表示该class
的attribute
数量。
attributes[attributes_count]
attribute
集合。单个attributes
的基本结构如下:
|
|
跟常量池内的条目一样,前面两项attribute_name_index
和attribute_length
是所有attribute
共有的,info[attribute_length]
根据属性的不同使用不同的格式。
这里的attribute_length
长度是指其后内容的字节数,跟内容的表示方式无关,有的属性长度值是固定的,有的是不固定的。
比如,ConstantValue
,长度就是2:
|
|
属性的内容非常多,也很复杂,展开的话超出了本文的范围,可以参考虚拟机规范。
2 还原一个class文件的内容
根据上面的知识,我们按class文件的字节流顺序还原其内容。
首先,我们构造一个类,让它实现1个空的接口,继承1个空的父类,定义2个属性,1个方法,使用1个空的注解。
类Test
|
|
空的接口IA
|
|
空的父类SuperTest
|
|
空的注解MyAnnotation
,保留至CLASS
级别
|
|
编译后,用16进制查看Test.class
文件,内容如下:
|
|
下面我们按相同的顺序分析这些字节码(建议把字节码复制出来,一边分析一边删除):
magic
CA FE BA BE
4个字节
minor_version
00 00
,次版本号,0
major_version
00 37
,主版本号,3x16+7=55
constant_pool_count
00 2B
,常量池数量,2x16+11=43,表示常量池有42个具体的条目
constant_pool[constant_pool_count-1]
分别列出42个条目(手动分析前面的条目时还不知道后面条目的内容,这种情况都是回头补充的):
1 CONSTANT_Methodref
0A
开头,说明是CONSTANT_Methodref
类型,根据结构:
|
|
class_index:00 08
,第8个条目,SuperTest
name_and_type_index:00 1B
,第27个条目,<init>:()V
2 CONSTANT_Fieldref
09
开头,说明是CONSTANT_Fieldref
类型,根据结构:
|
|
class_index:00 07
,第7个条目,Test
name_and_type_index:00 1C
,第28个条目,a:I
3 CONSTANT_String
08
开头,说明是CONSTANT_String
类型,根据结构:
|
|
string_index:00 1D
,第29个条目,hello world
4 CONSTANT_Fieldref
09
开头,说明是CONSTANT_Fieldref
类型,根据结构:
|
|
class_index:00 07
,第7个条目,Test
name_and_type_index:00 1E
,第30个条目,s:Ljava/lang/String;
5 CONSTANT_Fieldref
09
开头,说明是CONSTANT_Fieldref
类型,根据结构:
|
|
class_index:00 1F
,第31个条目,java/lang/System
name_and_type_index:00 20
第32个条目,out:Ljava/io/PrintStream;
6 CONSTANT_Methodref
0A
开头,说明是CONSTANT_Methodref
类型,根据结构:
|
|
class_index:00 21
,第33个条目,java/io/PrintStream
name_and_type_index:00 22
,第34个条目,println:(Ljava/lang/String;)V
7 CONSTANT_Class
07
开头,说明是CONSTANT_Class
类型,根据结构:
|
|
name_index:00 23
,第35个条目,Test
8 CONSTANT_Class
07
开头,说明是CONSTANT_Class
类型,根据结构:
|
|
name_index:00 24
,第36个条目,SuperTest
9 CONSTANT_Class
07
开头,说明是CONSTANT_Class
类型,根据结构:
|
|
name_index:00 25
,第37个条目,IA
10 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 01
,长度1个字节
bytes[1]:0x61=6x16+1=97,查ascii表得知是:a
11 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 01
,长度1个字节
bytes[1]:0x49=4x16+9=73,查ascii表得知是:I
12 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 01
,长度1个字节
bytes[1]:0x73=7x16+3=115,查ascii表得知是:s
13 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 12
,长度0x12=1x16+2=18个字节
bytes[18]:4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
,对应的ascii值是L j a v a / l a n g / S t r i n g ;
14 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 06
,长度6个字节
bytes[6]:3C 69 6E 69 74 3E
,对应的ascii值是< i n i t >
15 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 03
,长度3个字节
bytes[3]:28 29 56
,对应的ascii值是( ) V
16 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 04
,长度4个字节
bytes[4]:43 6F 64 65
,对应的ascii值是C o d e
17 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 0F
,长度15个字节
bytes[15]:4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
,对应的ascii值是L i n e N u m b e r T a b l e
18 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 12
,长度0x12=1x16+2=18个字节
bytes[18]:4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65
,对应的ascii值是L o c a l V a r i a b l e T a b l e
19 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 04
,长度4个字节
bytes[4]:74 68 69 73
,对应的ascii值是t h i s
20 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 06
,长度6个字节
bytes[6]:4C 54 65 73 74 3B
,对应的ascii值是L T e s t ;
21 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 08
,长度8个字节
bytes[8]:73 61 79 48 65 6C 6C 6F
,对应的ascii值是s a y H e l l o
22 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 15
,长度0x15=1*16+5=21个字节
bytes[21]:28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
,对应的ascii值是( L j a v a / l a n g / S t r i n g ; ) V
23 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 0A
,长度10个字节
bytes[10]:53 6F 75 72 63 65 46 69 6C 65
,对应的ascii值是S o u r c e F i l e
24 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 09
,长度9个字节
bytes[9]:54 65 73 74 2E 6A 61 76 61
,对应的ascii值是T e s t . j a v a
25 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 1B
,长度0x1B=1x16+11=27个字节
bytes[27]:52 75 6E 74 69 6D 65 49 6E 76 69 73 69 62 6C 65 41 6E 6E 6F 74 61 74 69 6F 6E 73
,对应的ascii值是R u n t i m e I n v i s i b l e A n n o t a t i o n s
26 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 0E
,长度14个字节
bytes[14]:4C 4D 79 41 6E 6E 6F 74 61 74 69 6F 6E 3B
,对应的ascii值是L M y A n n o t a t i o n ;
27 CONSTANT_NameAndType
0C
开头,说明是CONSTANT_NameAndType
类型,根据结构:
|
|
name_index:00 0E
,第14个条目,<init>
descriptor_index:00 0F
,第15个条目,()V
28 CONSTANT_NameAndType
0C
开头,说明是CONSTANT_NameAndType
类型,根据结构:
|
|
name_index:00 0A
,第10个条目,a
descriptor_index:00 0B
,第11个条目,I
29 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 0B
,长度11个字节
bytes[11]:68 65 6C 6C 6F 20 77 6F 72 6C 64
,对应的ascii值是h e l l o 空格 w o r l d
30 CONSTANT_NameAndType
0C
开头,说明是CONSTANT_NameAndType
类型,根据结构:
|
|
name_index:00 0C
,第12个条目,s
descriptor_index:00 0D
,第13个条目,Ljava/lang/String;
31 CONSTANT_Class
07
开头,说明是CONSTANT_Class
类型,根据结构:
|
|
name_index:00 26
,第0x26=2x16+6=38个条目,java/lang/System
32 CONSTANT_NameAndType
0C
开头,说明是CONSTANT_NameAndType
类型,根据结构:
|
|
name_index:00 27
,第0x27=2x16+7=39个条目,out
descriptor_index:00 28
,第0x28=2x16+8=40个条目,Ljava/io/PrintStream;
33 CONSTANT_Class
07
开头,说明是CONSTANT_Class
类型,根据结构:
|
|
name_index:00 29
,第0x29=2x16+9=41个条目,java/io/PrintStream
34 CONSTANT_NameAndType
0C
开头,说明是CONSTANT_NameAndType
类型,根据结构:
|
|
name_index:00 2A
,第0x2A=2x16+10=42个条目,println
descriptor_index:00 16
,第0x16=1x16+6=22个条目,(Ljava/lang/String;)V
35 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 04
,长度4个字节
bytes[4]:54 65 73 74
,对应的ascii值是T e s t
36 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 09
,长度9个字节
bytes[9]:53 75 70 65 72 54 65 73 74
,对应的ascii值是S u p e r T e s t
37 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 02
,长度2个字节
bytes[2]:49 41
,对应的ascii值是IA
38 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 10
,长度0x10=1x16+0=16个字节
bytes[16]:6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
,对应的ascii值是j a v a / l a n g / S y s t e m
39 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 03
,长度3个字节
bytes[3]:6F 75 74
,对应的ascii值是o u t
40 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 15
,长度0x15=1x16+5=21个字节
bytes[21]:4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
,对应的ascii值是L j a v a / i o / P r i n t S t r e a m ;
41 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 13
,长度0x13=1x16+3=19个字节
bytes[19]:6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
,对应的ascii值是j a v a / i o / P r i n t S t r e a m
42 CONSTANT_Utf8
01
开头,说明是CONSTANT_Utf8
类型,根据结构:
|
|
length:00 07
,长度7个字节
bytes[7]:70 72 69 6E 74 6C 6E
,对应的ascii值是p r i n t l n
access_flags
00 21
,表示0020+0001
,所以是ACC_SUPER
,ACC_PUBLIC
,这里的ACC_SUPER
是为了兼容性存在的,java 8以后每个class
文件都有,现在已经没有实际意义。
this_class
00 07
,指向常量池第7个条目,即Test
super_class
00 08
,指向常量池第8个条目,即SuperTest
interfaces_count
00 01
,接口数量,1个
interfaces[]
指向常量池的条目,因为这里只有1个接口,所以取2个字节,00 09
,第9个条目,即IA
fields_count
00 02
,field
数量,2条。
fields[fields_count]
因为有2个field
,根据下面的基本结构分析:
|
|
第1个field
- access_flags:
00 02
,ACC_PRIVATE
- name_index:
00 0A
,指向第10个条目,即a
- descriptor_index:
00 0B
,指向第11个条目,即I
- attributes_count:
00 00
,0个 - attributes[attributes_count],因为是0个,所以这里没有。
第2个field
- access_flags:
00 00
,没有FLAG - name_index:
00 0C
,指向第12个条目,即s
- descriptor_index:
00 0D
,指向第13个条目,即Ljava/lang/String;
- attributes_count:
00 00
,0个 - attributes[attributes_count],因为是0个,所以这里没有。
methods_count
00 02
,方法数量,2个。
methods[methods_count]
因为有2个method
,根据下面的基本结构分析:
|
|
第一个方法
access_flags:
00 01
,即ACC_PUBLIC
name_index:
00 0E
,指向第13个条目,即Ljava/lang/String;
descriptor_index:
00 0F
,第向第14个条目,即<init>
attributes_count:
00 01
,属性数量,1条attributes[1]:属性内容,根据下面的结构展开:
1 2 3 4 5
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
attribute_name_index:
00 10
,第16个条目,即Code
,说明我们的属性是Code
attribute_length:00 00 00 42
,长度0x42=4x16+2=66个字节 info[66]:根据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]; }
max_stack:
00 02
,栈的最大值,2
max_locals:00 01
,本地变量的最大值,1
code_length:00 00 00 10
,code长度,16字节
code[16]:2A B7 00 01 2A 04 B5 00 02 2A 12 03 B5 00 04 B1
,这些对应的是虚拟机指令,查规范可得分别是aload_0 invokespecial indexbyte1=0 indexbyte2=1 aload_0 iconst_1 putfield indexbyte1=0 indexbyte2=2 aload_0 ldc index=3 putfield indexbyte1=0 indexbyte2=4 return
,其中indexbyte
是用来给它前面的指令加载常量池用的地址,地址的值是indexbyte1<<8 | indexbyte2
,就是两个字节表示的数,上面的指令整理后如下:1 2 3 4 5 6 7 8 9
aload_0 invokespecial #1 SuperTest.<init>()V aload_0 iconst_1 putfield #2 Test.a:I aload_0 ldc #3 hello world putfield #4 Test.s:Ljava/lang/String; return
exception_table_length:
00 00
,长度0。
exception_table[exception_table_length]:因为上面的长度是0,所以这里是空。
attributes_count:00 02
,属性数量,2个。 attributes[2]:2个属性,分别展开:- 第一个属性:
attribute_name_index:
00 11
,第0x11=1x16+1=17个条目,即LineNumberTable
,其结构如下:1 2 3 4 5 6 7 8
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]; }
attribute_length:
00 00 00 0E
,长度,14个字节line_number_table_length:
00 03
表格长度,3line_number_table[3]:属性长度14个字节,减去上面表示表格长度的2个字节,正好12字节,
00 00 00 02 00 04 00 03 00 09 00 04
分别对应当前的[0,2],[4,3],[9,4]
- 第二个属性:
- attribute_name_index:
00 12
,第0x12=1x16+2=18个条目,即LocalVariableTable
,其结构如下:1 2 3 4 5 6 7 8 9 10 11
LocalVariableTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; }
- attribute_length:
00 00 00 0C
,属性长度,12个字节 - local_variable_table_length:
00 01
,表格长度,1 - local_variable_table[1]:属性长度12个字节,减去上面表示表格长度的2个字节,正好10字节,
00 00 00 10 00 13 00 14 00 00
,对应里面的5个属性: - start_pc:
00 00
,0 - length:
00 10
,16 - name_index:
00 13
,第19个条目,this
- descriptor_index:
00 14
,第20个条目,LTest
- index:
00 00
,0
- attribute_name_index:
回头看方法的
attribute
里的info
是66个字节,把上面分析的内容加起来正好也是66字节。- 第一个属性:
第二个方法
再看一遍方法的结构:
|
|
- access_flags:
00 01
,即ACC_PUBLIC
- name_index:
00 15
,指向第21个条目,即sayHello
- descriptor_index:
00 16
,第向第22个条目,即(Ljava/lang/String;)V
- attributes_count:
00 01
,属性数量,1条 - attributes[1]:属性内容,先看名称
00 10
,指向第16个条目,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]; }
attribute_length:
00 00 00 40
,属性长度64字节max_stack:
00 02
,2max_locals:
00 02
,2code_length:
00 00 00 08
,长度8code[8]:
B2 00 05 2B B6 00 06 B1
,分别对应getstatic indexbyte1 indexbyte2 aload_1 invokevirtual indexbyte1 indexbyte2 return
,整理得:1 2 3 4
getstatic #5 java/lang/System.out:Ljava/io/PrintStream; aload_1 invokevirtual #6 java/io/PrintStream.println:(Ljava/lang/String;)V return
exception_table_length:
00 00
exception_table[0]:空
attributes_count:
00 02
,属性数量2个
第一个属性:- attribute_name_index:
00 11
,第17个条目,即LineNumberTable
,有如下结构:
1 2 3 4 5 6 7 8
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]; }
- attribute_length:
00 00 00 0A
,属性长度10 - line_number_table_length:
00 02
,表格长度,2 - line_number_table[2]:
00 00 00 06 00 07 00 07
,对应[0,6],[7,7] 第二个属性: - attribute_name_index:
00 12
,第18个条目,即LocalVariabletable
,有如下结构:
1 2 3 4 5 6 7 8 9 10 11
LocalVariableTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; }
- attribute_length:
00 00 00 16
,属性长度0x16=1x16+6=22字节 - local_variable_table_length:
00 02
表格长度,2 - local_variable_table[2]:表格长度2,内容如下:
{ start_pc:00 00
,0 length:00 08
,8 name_index:00 13
,第19个条目,this
descriptor_index:00 14
,第20个条目,LTest
index:00 00
,0 }, { start_pc:00 00
,0 length:00 08
,8 name_index:00 0C
,第12个条目,s
descriptor_index:00 0D
,第13个条目,Ljava/lang/String;
index:00 01
,1 }
这里的start_pc
和length
指的是前面Code_attribute
内的code[code_length]
的索引,表明当前这个local varialble
在第start_pc
(包含)个索引对应的操作到第start_pc+length
(不包含)个索引对应的操作期间一直有效。
- attribute_name_index:
attributes_count
00 02
,class
的属性数量,2个
attributes[attributes_count]
2个属性的内容:
第一个属性
attribute_name_index:
00 17
,第23个条目,SourceFile
,有如下结构:1 2 3 4 5
SourceFile_attribute { u2 attribute_name_index; u4 attribute_length; u2 sourcefile_index; }
attribute_length:
00 00 00 02
,属性长度,2,这是固定的,因为后面的内容固定为2字节sourcefile_index:
00 18
,源文件,第24个条目,Test.java
第二个属性
- attribute_name_index:
00 19
,第25个条目,RuntimeInvisibleAnnotations
,有如下结构:
1 2 3 4 5 6
RuntimeInvisibleAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; annotation annotations[num_annotations]; }
- attribute_length:属性长度,
00 00 00 06
,6个字节 - num_annotations:
00 01
,注解编号,1 - annotations[1]:注解内容,annotation有如下结构:
1 2 3 4 5 6 7
annotation { u2 type_index; u2 num_element_value_pairs; { u2 element_name_index; element_value value; } element_value_pairs[num_element_value_pairs]; }
- type_index:
00 1A
,注解类型,第26个条目,LMyAnnotation
- num_element_value_pairs:
00 00
,注解内的value及其赋值的数量,0 - element_value_pairs[0]:空
- type_index:
- attribute_name_index:
到这里文件内容就还原结束了,跟javap -v Test.class
的内容可以进行对比,基本一致。
3 JVM从加载class文件到Class初始化的过程
按照虚拟机规范的分类,这个过程可以分成3个步骤,但是这3个步骤也不是简单的顺序执行,而是根据需要交织在一起。
先介绍一下这3个步骤,再用一个例子说明。
注:class文件可以表示类或接口,jdk9以后还可以表示模块,因为模块用的不多,下文没有特殊说明的地方都默认表示类或接口。
3.1 创建和载入(Creation and Loading)
准确的说应该是"载入和创建”(为了跟类"加载"区分出来,这里loading译成载入而不是加载)。这个过程用classlader
把class
文件(或者其它表示形式如二进制数据流等)载入到虚拟机中转化成方法区
内部的表现形式(具体跟虚拟机实现有关),从而完成对应Class的创建。在载入时,虚拟机会执行格式检查(format checking),主要是检查class文件整体格式是否符合要求,常量池中的数据格式等,这只算是“表面”上的检查,因为还没有执行链接中的解析等操作,就算常量池中的引用类型对应的Class并不存在,在这个阶段也查不出来。在后面的链接阶段会执行更详细的检查。
在虚拟机的角度,只有两种classloader
:
Bootstrap Class Loader
,虚拟机内部实现的类加载器User-defined Class Loader
,除了Bootstrap之外的其它类加载器,都是ClassLoader
这个抽象类的子类
在用类加载器L
加载名字为N
的Class(记为C
)时,两种类加载器的表现有点区别:
第一步:两种类加载器都会首先判断当前类加载器L
是否已经被记录为名字为N
的类的初始化加载器(initiating loader),如果是,说明想要的Class已经被加载过,直接返回结果C
第二步:如果第一步没有找到对应的记录,那么:
Bootstrap Class Loader
:JVM把名字N
这个参数传递给当前加载器的一个方法,根据N
去搜索C
的一个"宣称合理但未经验证的表现形式"(purported representation),通常是一个文件,比如文件名是N
,找到后JVM会尝试用当前加载器从这个文件生成跟N
对应的C
。成功后,记L
为C
的initiating loader
注和defining loader
注。这里会有两种意外情况:- “宣称合理但未经验证的表现形式"找不到:抛出
ClassNotFoundException
异常的一个实例。 - “宣称合理但未经验证的表现形式"验证失败:有几种可能:文件结构不符合虚拟机规范、版本号不被当前虚拟机支持、文件所表示的类并不是
N
所表示的类(比如class
文件内的this_class
名字不是N
),这些情况都会抛出对应的异常。
- “宣称合理但未经验证的表现形式"找不到:抛出
User-defined Class Loader
:它跟bootstrap
的区别在于,这个加载器可以把创建操作委托给自己的父加载器(只是委托层次上的父加载器,不是实际的父子类继承关系),当然也可以自己直接创建,如果直接创建,跟上面bootstrap
的方式类拟。
注:
initiating loader:初始化类加载器,可以有多个,如果有委托层次L->LP,且L发起初始化,但委托给LP,LP是defining lader,完成创建,那么L和LP都是initiating loader
defining loader:从文件生成class的这个加载器,也就是实际完成创建的加载器,defining loader必定是initiating loader
关于数组:
数组类型本身是由虚拟机直接创建的,但它包含的元素类型的加载仍然是用的上面的方式。如果元素类型是引用类型,那么数组C
的defining loader
记录就是其元素类型的defining loader
,如果元素是原始类型,那么数组C
的defining loader
记录就是bootstrap class loader
,当然也同时包含initiating loader
的记录。
载入约束(loading constraints)
如果C
(<N1,L1>确定,表示L1是C
的defining loader
)里面的字段(字段类型)或者方法(参数类型,返回值类型)引用了其它Class比如D
(<N2,L2>),那么相应的字段或者方法的descriptor
所涉及到的任意类型,由L1,L2
作为初始类加载器加载的时候,其表示的结果应该是同一个类,即NL1=NL2。比如,C
里面的1个方法的descriptor
是(LE,LF)LG
,那么E,F,G
这3个名字分别用L1,L2
加载的时候,表示的类应该是相同的。
载入约束会在记录类的初始加载器的时候进行检查,后面提到的链接阶段也会触发检查。
3.2 链接(Linking)
链接过程可以细分为3个步骤:验证、准备、解析。虚拟机规范没有明确要求链接的操作什么时候执行,只要满足下面几个要求:
- class在被链接前必须完全被载入
- class在初始化之前必须完成验证、准备这两个步骤
- 如果链接过程会报错,有class跟这个错误有关,那么异常抛出的时机必须是:应用程序执行一些操作,直接或间接的需要链接这个class的时候。这可以让异常的抛出对调用方来说更加的明显。
- 对一个动态计算的常量的解析操作,会延迟执行,直到满足下面两种情况之一:
ldc,ldc_w,ldc2_w
这3个指令引用这个常量,并且执行bootstrap
加载器的方法把这个常量作为一个静态参数调用
3.2.1 验证(Verification)
这个阶段主要是检查Code_Attribute
的格式,因为这个属性内包含了类中的所有方法执行的代码,都是用虚拟机指令表示,属性本身非常复杂,检查的内容也很多,具体看虚拟机规范4.9,4.10
3.2.2 准备(Preparation)
这个阶段创建class的静态字段并初始化为系统的默认值,这个值是虚拟机指定的,比如int
类型是0,并不是代码中的初始化赋值。在这个阶段会执行上面提到的载入约束检查。
准备阶段可以在创建(creation)和初始化(initialization)之间的任意时间节点出现。
3.2.3 解析(Resolution)
这个阶段就是把常量池里面那些引用符号解析成具体的Class,在解析这些引用符号比如方法时,其中引用的类型会先完成解析。
这里的解析也包括父类和实现的接口等,这些被解析的Class重复上面的流程,先被载入、创建
,然后验证、准备、解析
。
3.3 初始化(Initialization)
class的初始化就是执行其<clinit>
方法,这个方法是被虚拟机直接执行的,这一步结束就代表类已经初始化,类中的静态字段和静态方法等已经可用。
只有以下情况之一才会触发初始化:
- 执行虚拟机的以下四个指令之一:
new, getstatic, putstatic,invokestatic
,new是初始化类的实例,显然需要先初始化类本身,后面3个就是对类的静态字段和方法的调用,也需要类本身。 - 以下四种
method handle
:kind 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial)
,对其中任何一个的解析结果是一个java.lang.invoke.MethodHandle
类型的实例,那么在第一次调用这个实例时会触发初始化。这条跟上面那条本质上是一样的,只不过把指令包装在Methodhandle
内。 - 对一些反射方法的调用,比如
java.lang.reflect
包内的一些方法。 - 对
C
的子类进行初始化之前,要先初始化C
- 如果
C
是一个接口,里面实现了一个non-static,non-abstract
的方法(也就是提供了一个default
方法时),假设D
直接或间接实现了C
,那么对D
的初始化会触发C
的初始化,这跟子类初始化触发父类差不多。 - 作为虚拟机的启动类,比如
java MyApp
,这里的MyApp
这个类会要求初始化。
上面的4,5两条,规定了一个类在初始化时,它的父类和提供了default
方法的接口也会被初始化,那么为什么普通的接口就算引用了它的静态字段也不会被初始化呢?
先看一下JLS12.4.1从java语言使用层面的说明,一个class,记为T,只有下面4种情况之一会触发T的初始化:
- T是类,当T被实例化时
- T中的一个静态方法被调用时
- T中的一个静态字段被赋值时
- T中的一个静态字段被使用且这个字段不是一个
constant variable
(俗称常量,被final修饰的原始类型或者String类型变量,且初始化值是一个常量表达式,如原始类型的值或者简单的四则运算或者String常量) - 子类的初始化会触发父类的初始化以及提供了
default
方法的接口的初始化,接口本身的初始化不会触发它的父接口的初始化。 - 一些反射调用
这里除了第4条以外,其它几条跟上面的虚拟机规范的内容都能呼应上,第4条意思就是对T的非常量字段的引用才会触发T的初始化,什么是常量,上面已经解释了,仔细阅读几遍,很多人都没有真的明白java中定义的常量是什么,举几个反例:
|
|
常量的定义比较严格,满足这些定义以后,对于编译器来说这个值就可以确定了。
还有一个问题,那些"普通的接口"是什么时候初始化的呢?比如只有几个方法签名的接口?
它们不需要初始化,跟常量一样,方法签名这些信息在编译时就确定了,没有必要初始化。
JLS12.4.1中也提供了一些示例代码,大家也可以自己构造一些类来验证上面的内容。
PS:上面提到的对于常量字段的使用,在class
文件中可以看到对应的访问指令是iconst_n
这个系列,而如果使用非常量字段,对应的指令就是getstatic
,符合前面虚拟机规范定义的情况。
类的卸载
简单提一下关于类的卸载,因为类加载的时候,类和类加载器之间存在相互引用,准确的说是强引用,如果要卸载一个类,首先类的所有实例都已经被回收,且必须同时卸载相应的类加载器,而这需要经过精心设计的自定义类加载器,所以类的卸载情况也很少发生(直接继承Classloader
重载load
方法的“自定义加载器”是不行的)。可以在启动时加入虚拟机参数-Xlog:class+unload
来观察卸载情况(大部分场景下看不到相关输出,因为没有发生卸载)。
4 双亲委派模型(parent-delegation model)
这个翻译容易让人误解,叫“父类委派模型”或许更直观。
简单的说,就是一个classloader
加载一个类时,会先把这个加载任务委派给自己的父加载器(只是委派模型中的父子关系,不是类的继承),父加载器也会委派给自己的父加载器。。。直到bootstrap
这个最基本的类加载器,它没有父加器,如果bootstrap
无法加载,那么再回退给委派给它的那个类加载器,如果这个类加载器也无法加载,再回退。。。直到回到最初的那个类加载器,调用findClass
方法,这个方法内容是由具体实现类重载的,如果这个加载器也无法加载,则会报错。
在ClassLoader
这个抽象类中,loadClass
方法包含了这个模型的逻辑:
显示代码
|
|
上面的parent!=null
用来判断是否是bootstrap
类加载器,null
代表bootstrap
加载器。
Java9引入模块化以后,类加载器的部分也发生了变化,其中的PlatformClassLoader
(取代了以前的ExtClassLoader
),AppClassLoader
都是继承自BuiltinClassLoader
,而BuiltinClassLoader
内的loadClassOrNull
方法加入了先判断类是否属于某个模块的逻辑,然后再执行父类委派,某种程度上已经破坏了双亲委派,所以后面不针对JAVA11,只是对传统意义上的双亲委派进行优缺点分析
下面这段代码可以查看jdk中所有模块对应的加载器,有兴趣的可以看一下模块化以后的类加载情况:
|
|
双亲委派的优点:
- 保证核心类的加载安全:我们用换一种方式来描述双亲委派机制的作用:越重要的类由越底层的类加载器加载,像
java.util
,java.lang
这些常用的核心库文件都是由bootstrap class loader
加载的,只有几个内置的加载器都不加载的类,才轮到自定义加载器加载。所以比如我们自定义了一个Integer
类型,编译后放在当前项目的class目录内,然后AppClassLoader
负责加载当前项目的类,假如没有双亲委派,那么自定义的这个Integer
类就会在这个阶段被加载,这对于核心类来说太容易被破坏了,除非虚拟机在加载class
时要不断的去判断这些class文件是否跟核心类库里的类冲突,就算是检测到冲突,也只能报运行时异常了。如果基于双亲委派模型,那么AppClassLoader
就会委派给ExtClassLoader
(jdk8以及前)或者PlatformClassLoader
(jdk9及以后),然后再委派给bootstrap class loader
,最后由bootstrap class loader
加载这个类,这就保证了所有用到Integer
等核心类库里的类的地方,就算有冲突,最终都会加载的核心类库里的类。
PS:所有类都是全限定名,比如
Integer
是java.lang.Integer
,所以上面的测试也要把Integer
放在java.lang
这个包内,但是直接代码结构定义这个包的话会在编译时报错,提示包名跟核心类库冲突,所以我们只能先在别的路径先编译好一个Integer.class
,然后在项目的class路径内创建java.lang
这个包把Integer.class
放进去
- 避免类的重复加载:如果没有用双亲委派模型,就算没有上面说的安全问题,且一个自定义类加载器在当前目录找不到class文件的时候,会自动去查找核心类库里的class文件,然后成功加载,那么100个自定义的类加载器就会有100次一模一样的加载,并且都把自己记录为这个class的初始类加载器(理论上来说也是define class loader)。如果用了双亲委派模型,比如
Integer
这个类,虽然是最终由bootstrap class loader
加载的,但是根据前面虚拟机规范定义的规则,发起这个加载的classloader,比如叫L1
,和bootstrap class loader
都会被记录为这个类的初始化加载器(initiating loader),后面如果L2
也要加载这个类,最终委派给bootstrap class loader
的时候,会直接返回结果,并把L2
也记录为initiating loader
,这样,同一个类,只需要由"最接近底层且符合要求"的类加载器加载一次,虽然这个加载器可能会被多次委派去加载这个类。
双亲委派的缺点:
因为双亲委派只能由“子加载器”向“父加载器”委派,而不能反向,很多时候这都没什么影响,比如我们编写一个普通的程序,要么用“核心类库”里的功能(向上委派由bootstarp class loader
或者PlatfromClassLoader
加载),要么调用由我们自己代码实现的功能(委派后回退到AppClassLoader
加载),但是在SPI或者一些框架的实现中,就不是这么简单。
比如,jdbc,只提供了一套接口,具体的实现是由不同的数据库厂商负责的,我们在编写代码的时候,都是面向java平台提供的接口编程,用一个例子说明问题:
|
|
上面的Connection
,DriverManager
都在java.sql
这个包下面,打印结果显示它们都是由PlatformClassLoader
加载,而第三方实现肯定是用AppClassLoader
或者自定义类加载器才能加载,如果用双亲委派的模型,上面的getConnection
方法在尝试加载具体实现类的时候,会尝试用自己的加载器(PlatformClassLoader
)去加载第三方类,但是无法加载,然后报错。
解决方法就是生成一个AppClassLoader
去加载第三方类,比如用Thread.currentThread().getContextClassLoader()
获取当前线程的加载器。
最后用文字简单记一下通过DriverManager
这个类为入口分析第三方实现类加载的过程:
- 首先
getConnection
调用内部的getConnection()
方法,这个方法内部有一个ensureDriversInitialized
方法 ensureDriversInitialized
方法内部有一个AccessController.doPrivileged
方法,其中使用了ServiceLoader
类ServiceLoader.load方法会设置一个类加载器变量1 2
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator();
ClassLoader cl = Thread.currentThread().getContextClassLoader()
,并在ServiceLoader
类的构造方法中赋值给变量loader
关键在于这个driversIterator
变量,它的操作会通过newLookupIterator
类型的变量来完成,当后面的代码调用driversIterator.hasNext()
的时候,触发懒加载机制,最终来到ServiceLoader
的内部类LazyClassPathLookupIterator
的hasNextService()
方法hasNextService()
方法里调用了nextProviderClass()
方法,这个方法去META-INF
下查找第三方实现类的信息,方法最终返回return Class.forName(cn, false, loader)
,这一步才加载真正的实现类,用的上面的变量loader
所定义的加载器
版权声明 本博客使用CC BY-NC-SA 4.0许可协议(创意共享4.0:保留署名-非商业性使用-相同方式共享)。