本文先简单介绍虚拟机规范约定的class文件格式,并从一个示例class文件复原代码基本内容,最后描述虚拟机类加载机制以及介绍一下双亲委派模型。Java版本为11。

1 class文件格式

class文件是字节码文件,文件内容基本单位就是一个字节,文件中不同的项根据约定的格式用一个或多个字节表示。
下面是class文件的基本结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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];
}

在分别介绍上面的每一个条目之前,先说明几点:

  • “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 SEclass file format version range
1.0.245.0 ≤ v ≤ 45.3
1.145.0 ≤ v ≤ 45.65535
1.245.0 ≤ v ≤ 46.0
1.345.0 ≤ v ≤ 47.0
1.445.0 ≤ v ≤ 48.0
5.045.0 ≤ v ≤ 49.0
645.0 ≤ v ≤ 50.0
745.0 ≤ v ≤ 51.0
845.0 ≤ v ≤ 52.0
945.0 ≤ v ≤ 53.0
1045.0 ≤ v ≤ 54.0
1145.0 ≤ v ≤ 55.0

constant_pool_count

2个字节。常量池条目的数量,值是“实际条目数量+1”,一个说明是因为计算的时候把constant_pool_count本身也算在内了,其下标是0。

constant_pool[constant_pool_count-1]

常量池,对应的条目下标编号是从1开始,直到constant_pool_count-1,每一个条目的基本结构如下:

1
2
3
4
cp_info {
    u1 tag;
    u1 info[];
}

tag表是这个条目是什么类型的,info[]表示后面的具体信息。
比如,tag是1表示CONSTANT_Utf8,对应的结构如下:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

注意,这里的tag和上面tag是同一个,也就是说,length和bytes[length]替换了info[]的位置。
全部的常量池类型参考虚拟机规范

access_flags

2个字节。类型的一些信息,可以是几个值的组合,每个FLAG信息如下:

Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
ACC_FINAL0x0010Declared final; no subclasses allowed.
ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE0x0200Is an interface, not a class.
ACC_ABSTRACT0x0400Declared abstract; must not be instantiated.
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
ACC_ANNOTATION0x2000Declared as an annotation type.
ACC_ENUM0x4000Declared as an enum type.
ACC_MODULE0x8000Is 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的基本结构如下:

1
2
3
4
5
6
7
field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • access_flags:跟前面描述的access_flags区别就是classfield可以匹配的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;
  • attributes_count:属性的数量
  • attributes[attributes_count]:属性的具体内容。属性这个条目可能在类、方法、字段这些条目中出现,我们放到最后类的属性时再讲。

methods_count

2个字节。表示method的数量。

methods[methods_count]

method集合。单个method的基本结构如下:

1
2
3
4
5
6
7
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • methodfield基本结构是一样的,只说不同点: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;

attributes_count

2个字节。表示该classattribute数量。

attributes[attributes_count]

attribute集合。单个attributes的基本结构如下:

1
2
3
4
5
attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

常量池内的条目一样,前面两项attribute_name_indexattribute_length是所有attribute共有的,info[attribute_length]根据属性的不同使用不同的格式。
这里的attribute_length长度是指其后内容的字节数,跟内容的表示方式无关,有的属性长度值是固定的,有的是不固定的。
比如,ConstantValue,长度就是2:

1
2
3
4
5
ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

属性的内容非常多,也很复杂,展开的话超出了本文的范围,可以参考虚拟机规范

2 还原一个class文件的内容

根据上面的知识,我们按class文件的字节流顺序还原其内容。

首先,我们构造一个类,让它实现1个空的接口,继承1个空的父类,定义2个属性,1个方法,使用1个空的注解。
Test

1
2
3
4
5
6
7
8
@MyAnnotation
public class Test extends SuperTest implements IA{
    private int a=1;
    String s="hello world";
    public static void sayHello(String aString,Integer anInt,double aDouble){
        System.out.println(aString);
    }
}

空的接口IA

1
2
public interface IA {
}

空的父类SuperTest

1
2
public class SuperTest {
}

空的注解MyAnnotation,保留至CLASS级别

1
2
3
4
5
6
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {
}

编译后,用16进制查看Test.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
CA FE BA BE 00 00 00 37 00 2B 0A 00 08 00 1B 09 
00 07 00 1C 08 00 1D 09 00 07 00 1E 09 00 1F 00 
20 0A 00 21 00 22 07 00 23 07 00 24 07 00 25 01 
00 01 61 01 00 01 49 01 00 01 73 01 00 12 4C 6A 
61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 
01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 
00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 
62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 
56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 
74 68 69 73 01 00 06 4C 54 65 73 74 3B 01 00 08 
73 61 79 48 65 6C 6C 6F 01 00 15 28 4C 6A 61 76 
61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 
01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 09 
54 65 73 74 2E 6A 61 76 61 01 00 1B 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 01 00 0E 4C 4D 79 41 6E 6E 
6F 74 61 74 69 6F 6E 3B 0C 00 0E 00 0F 0C 00 0A 
00 0B 01 00 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64 
0C 00 0C 00 0D 07 00 26 0C 00 27 00 28 07 00 29 
0C 00 2A 00 16 01 00 04 54 65 73 74 01 00 09 53 
75 70 65 72 54 65 73 74 01 00 02 49 41 01 00 10 
6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 
01 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 
6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 
13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 
72 65 61 6D 01 00 07 70 72 69 6E 74 6C 6E 00 21 
00 07 00 08 00 01 00 09 00 02 00 00 00 0A 00 0B 
00 00 00 00 00 0C 00 0D 00 00 00 02 00 01 00 0E 
00 0F 00 01 00 10 00 00 00 42 00 02 00 01 00 00 
00 10 2A B7 00 01 2A 04 B5 00 02 2A 12 03 B5 00 
04 B1 00 00 00 02 00 11 00 00 00 0E 00 03 00 00 
00 02 00 04 00 03 00 09 00 04 00 12 00 00 00 0C 
00 01 00 00 00 10 00 13 00 14 00 00 00 01 00 15 
00 16 00 01 00 10 00 00 00 40 00 02 00 02 00 00 
00 08 B2 00 05 2B B6 00 06 B1 00 00 00 02 00 11 
00 00 00 0A 00 02 00 00 00 06 00 07 00 07 00 12 
00 00 00 16 00 02 00 00 00 08 00 13 00 14 00 00 
00 00 00 08 00 0C 00 0D 00 01 00 02 00 17 00 00 
00 02 00 18 00 19 00 00 00 06 00 01 00 1A 00 00

下面我们按相同的顺序分析这些字节码(建议把字节码复制出来,一边分析一边删除):

magic

CA FE BA BE4个字节

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类型,根据结构:

1
2
3
4
5
CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_index:00 08,第8个条目,SuperTest
name_and_type_index:00 1B,第27个条目,<init>:()V

2 CONSTANT_Fieldref

09开头,说明是CONSTANT_Fieldref类型,根据结构:

1
2
3
4
5
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_index:00 07,第7个条目,Test
name_and_type_index:00 1C,第28个条目,a:I

3 CONSTANT_String

08开头,说明是CONSTANT_String类型,根据结构:

1
2
3
4
CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

string_index:00 1D,第29个条目,hello world

4 CONSTANT_Fieldref

09开头,说明是CONSTANT_Fieldref类型,根据结构:

1
2
3
4
5
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_index:00 07,第7个条目,Test
name_and_type_index:00 1E,第30个条目,s:Ljava/lang/String;

5 CONSTANT_Fieldref

09开头,说明是CONSTANT_Fieldref类型,根据结构:

1
2
3
4
5
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

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类型,根据结构:

1
2
3
4
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index:00 23,第35个条目,Test

8 CONSTANT_Class

07开头,说明是CONSTANT_Class类型,根据结构:

1
2
3
4
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index:00 24,第36个条目,SuperTest

9 CONSTANT_Class

07开头,说明是CONSTANT_Class类型,根据结构:

1
2
3
4
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index:00 25,第37个条目,IA

10 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 01,长度1个字节
bytes[1]:0x61=6x16+1=97,查ascii表得知是:a

11 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 01,长度1个字节
bytes[1]:0x49=4x16+9=73,查ascii表得知是:I

12 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 01,长度1个字节
bytes[1]:0x73=7x16+3=115,查ascii表得知是:s

13 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 06,长度6个字节
bytes[6]:3C 69 6E 69 74 3E,对应的ascii值是< i n i t >

15 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 03,长度3个字节
bytes[3]:28 29 56,对应的ascii值是( ) V

16 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 04,长度4个字节
bytes[4]:43 6F 64 65,对应的ascii值是C o d e

17 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 04,长度4个字节
bytes[4]:74 68 69 73,对应的ascii值是t h i s

20 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 06,长度6个字节
bytes[6]:4C 54 65 73 74 3B,对应的ascii值是L T e s t ;

21 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

name_index:00 0E,第14个条目,<init>
descriptor_index:00 0F,第15个条目,()V

28 CONSTANT_NameAndType

0C开头,说明是CONSTANT_NameAndType类型,根据结构:

1
2
3
4
5
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

name_index:00 0A,第10个条目,a
descriptor_index:00 0B,第11个条目,I

29 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

name_index:00 0C,第12个条目,s
descriptor_index:00 0D,第13个条目,Ljava/lang/String;

31 CONSTANT_Class

07开头,说明是CONSTANT_Class类型,根据结构:

1
2
3
4
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index:00 26,第0x26=2x16+6=38个条目,java/lang/System

32 CONSTANT_NameAndType

0C开头,说明是CONSTANT_NameAndType类型,根据结构:

1
2
3
4
5
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

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类型,根据结构:

1
2
3
4
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index:00 29,第0x29=2x16+9=41个条目,java/io/PrintStream

34 CONSTANT_NameAndType

0C开头,说明是CONSTANT_NameAndType类型,根据结构:

1
2
3
4
5
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 04,长度4个字节
bytes[4]:54 65 73 74,对应的ascii值是T e s t

36 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 02,长度2个字节
bytes[2]:49 41,对应的ascii值是IA

38 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length:00 03,长度3个字节
bytes[3]:6F 75 74,对应的ascii值是o u t

40 CONSTANT_Utf8

01开头,说明是CONSTANT_Utf8类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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类型,根据结构:

1
2
3
4
5
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

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 02field数量,2条。

fields[fields_count]

因为有2个field,根据下面的基本结构分析:

1
2
3
4
5
6
7
field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info ;
}

第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,根据下面的基本结构分析:

1
2
3
4
5
6
7
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

第一个方法

  • 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表格长度,3

      • line_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里的info是66个字节,把上面分析的内容加起来正好也是66字节。

第二个方法

再看一遍方法的结构:

1
2
3
4
5
6
7
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • 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,2

    • max_locals:00 02,2

    • code_length:00 00 00 08,长度8

    • code[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_pclength指的是前面Code_attribute内的code[code_length]的索引,表明当前这个local varialble在第start_pc(包含)个索引对应的操作到第start_pc+length(不包含)个索引对应的操作期间一直有效。

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]:空

到这里文件内容就还原结束了,跟javap -v Test.class的内容可以进行对比,基本一致。

3 JVM从加载class文件到Class初始化的过程

按照虚拟机规范的分类,这个过程可以分成3个步骤,但是这3个步骤也不是简单的顺序执行,而是根据需要交织在一起。
先介绍一下这3个步骤,再用一个例子说明。
注:class文件可以表示类或接口,jdk9以后还可以表示模块,因为模块用的不多,下文没有特殊说明的地方都默认表示类或接口。

3.1 创建和载入(Creation and Loading)

准确的说应该是"载入和创建”(为了跟类"加载"区分出来,这里loading译成载入而不是加载)。这个过程用classladerclass文件(或者其它表示形式如二进制数据流等)载入到虚拟机中转化成方法区内部的表现形式(具体跟虚拟机实现有关),从而完成对应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。成功后,记LCinitiating loaderdefining 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

关于数组:
数组类型本身是由虚拟机直接创建的,但它包含的元素类型的加载仍然是用的上面的方式。如果元素类型是引用类型,那么数组Cdefining loader记录就是其元素类型的defining loader,如果元素是原始类型,那么数组Cdefining loader记录就是bootstrap class loader,当然也同时包含initiating loader的记录。

载入约束(loading constraints)
如果C(<N1,L1>确定,表示L1是Cdefining 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>方法,这个方法是被虚拟机直接执行的,这一步结束就代表类已经初始化,类中的静态字段和静态方法等已经可用。
只有以下情况之一才会触发初始化

  1. 执行虚拟机的以下四个指令之一:new, getstatic, putstatic,invokestatic,new是初始化类的实例,显然需要先初始化类本身,后面3个就是对类的静态字段和方法的调用,也需要类本身。
  2. 以下四种method handlekind 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial),对其中任何一个的解析结果是一个java.lang.invoke.MethodHandle类型的实例,那么在第一次调用这个实例时会触发初始化。这条跟上面那条本质上是一样的,只不过把指令包装在Methodhandle内。
  3. 对一些反射方法的调用,比如java.lang.reflect包内的一些方法。
  4. C的子类进行初始化之前,要先初始化C
  5. 如果C是一个接口,里面实现了一个non-static,non-abstract的方法(也就是提供了一个default方法时),假设D直接或间接实现了C,那么对D的初始化会触发C的初始化,这跟子类初始化触发父类差不多。
  6. 作为虚拟机的启动类,比如java MyApp,这里的MyApp这个类会要求初始化。

上面的4,5两条,规定了一个类在初始化时,它的父类和提供了default方法的接口也会被初始化,那么为什么普通的接口就算引用了它的静态字段也不会被初始化呢?
先看一下JLS12.4.1从java语言使用层面的说明,一个class,记为T,只有下面4种情况之一会触发T的初始化:

  1. T是类,当T被实例化时
  2. T中的一个静态方法被调用时
  3. T中的一个静态字段被赋值时
  4. T中的一个静态字段被使用且这个字段不是一个constant variable(俗称常量,被final修饰的原始类型或者String类型变量,且初始化值是一个常量表达式,如原始类型的值或者简单的四则运算或者String常量)
  5. 子类的初始化会触发父类的初始化以及提供了default方法的接口的初始化,接口本身的初始化不会触发它的父接口的初始化。
  6. 一些反射调用

这里除了第4条以外,其它几条跟上面的虚拟机规范的内容都能呼应上,第4条意思就是对T的非常量字段的引用才会触发T的初始化,什么是常量,上面已经解释了,仔细阅读几遍,很多人都没有真的明白java中定义的常量是什么,举几个反例:

1
2
3
static int a=0;//没有final修饰
static final Integer a=0;//不是原始类型
static final int=Integer.valueOf(0);//不符合常量表达式要求,返回的是Integer,还要拆箱才是int

常量的定义比较严格,满足这些定义以后,对于编译器来说这个值就可以确定了。
还有一个问题,那些"普通的接口"是什么时候初始化的呢?比如只有几个方法签名的接口?
它们不需要初始化,跟常量一样,方法签名这些信息在编译时就确定了,没有必要初始化。

JLS12.4.1中也提供了一些示例代码,大家也可以自己构造一些类来验证上面的内容。

PS:上面提到的对于常量字段的使用,在class文件中可以看到对应的访问指令是iconst_n这个系列,而如果使用非常量字段,对应的指令就是getstatic,符合前面虚拟机规范定义的情况

类的卸载

简单提一下关于类的卸载,因为类加载的时候,类和类加载器之间存在相互引用,准确的说是强引用,如果要卸载一个类,首先类的所有实例都已经被回收,且必须同时卸载相应的类加载器,而这需要经过精心设计的自定义类加载器,所以类的卸载情况也很少发生(直接继承Classloader重载load方法的“自定义加载器”是不行的)。可以在启动时加入虚拟机参数-Xlog:class+unload来观察卸载情况(大部分场景下看不到相关输出,因为没有发生卸载)。

4 双亲委派模型(parent-delegation model)

这个翻译容易让人误解,叫“父类委派模型”或许更直观。
简单的说,就是一个classloader加载一个类时,会先把这个加载任务委派给自己的父加载器(只是委派模型中的父子关系,不是类的继承),父加载器也会委派给自己的父加载器。。。直到bootstrap这个最基本的类加载器,它没有父加器,如果bootstrap无法加载,那么再回退给委派给它的那个类加载器,如果这个类加载器也无法加载,再回退。。。直到回到最初的那个类加载器,调用findClass方法,这个方法内容是由具体实现类重载的,如果这个加载器也无法加载,则会报错。
ClassLoader这个抽象类中,loadClass方法包含了这个模型的逻辑:

显示代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

上面的parent!=null用来判断是否是bootstrap类加载器,null代表bootstrap加载器。

Java9引入模块化以后,类加载器的部分也发生了变化,其中的PlatformClassLoader(取代了以前的ExtClassLoader),AppClassLoader都是继承自BuiltinClassLoader,而BuiltinClassLoader内的loadClassOrNull方法加入了先判断类是否属于某个模块的逻辑,然后再执行父类委派,某种程度上已经破坏了双亲委派,所以后面不针对JAVA11,只是对传统意义上的双亲委派进行优缺点分析
下面这段代码可以查看jdk中所有模块对应的加载器,有兴趣的可以看一下模块化以后的类加载情况:

1
2
3
4
5
6
7
class Test{
    public static void main(String[] args) throws ClassNotFoundException {
        for(Module m:ModuleLayer.boot().modules()){
            System.out.println(m.getName()+"====>"+m.getClassLoader());
        }
    }
}

双亲委派的优点:

  • 保证核心类的加载安全:我们用换一种方式来描述双亲委派机制的作用:越重要的类由越底层的类加载器加载,像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:所有类都是全限定名,比如Integerjava.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平台提供的接口编程,用一个例子说明问题:

1
2
3
4
5
6
7
8
public class test {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb","root","123456");
        //看一下这两个类的classloader
        System.out.println(Connection.class.getClassLoader());
        System.out.println(DriverManager.class.getClassLoader());
    }
}

上面的Connection,DriverManager都在java.sql这个包下面,打印结果显示它们都是由PlatformClassLoader加载,而第三方实现肯定是用AppClassLoader或者自定义类加载器才能加载,如果用双亲委派的模型,上面的getConnection方法在尝试加载具体实现类的时候,会尝试用自己的加载器(PlatformClassLoader)去加载第三方类,但是无法加载,然后报错。
解决方法就是生成一个AppClassLoader去加载第三方类,比如用Thread.currentThread().getContextClassLoader()获取当前线程的加载器。

最后用文字简单记一下通过DriverManager这个类为入口分析第三方实现类加载的过程:

  • 首先getConnection调用内部的getConnection()方法,这个方法内部有一个ensureDriversInitialized方法
  • ensureDriversInitialized方法内部有一个AccessController.doPrivileged方法,其中使用了ServiceLoader
    1
    2
    
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
    ServiceLoader.load方法会设置一个类加载器变量ClassLoader cl = Thread.currentThread().getContextClassLoader(),并在ServiceLoader类的构造方法中赋值给变量loader
    关键在于这个driversIterator变量,它的操作会通过newLookupIterator类型的变量来完成,当后面的代码调用driversIterator.hasNext()的时候,触发懒加载机制,最终来到ServiceLoader的内部类LazyClassPathLookupIteratorhasNextService()方法
  • hasNextService()方法里调用了nextProviderClass()方法,这个方法去META-INF下查找第三方实现类的信息,方法最终返回return Class.forName(cn, false, loader),这一步才加载真正的实现类,用的上面的变量loader所定义的加载器