JVM类加载机制

Java类加载机制

参考

1. 字节码

源码:

test.java

1
2
3
4
5
6
7
8
package Test;

/*这个文件主要用来做java课程作业*/
public class test {
public static void main(String[] args){
System.out.println("this is a test");
}
}

编译成功后,用xxd test.class 命令可以查看一下这个字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00000000: cafe babe 0000 0038 0010 0a00 0300 0d07  .......8........
00000010: 000e 0700 0f01 0006 3c69 6e69 743e 0100 ........<init>..
00000020: 0328 2956 0100 0443 6f64 6501 000f 4c69 .()V...Code...Li
00000030: 6e65 4e75 6d62 6572 5461 626c 6501 0012 neNumberTable...
00000040: 4c6f 6361 6c56 6172 6961 626c 6554 6162 LocalVariableTab
00000050: 6c65 0100 0474 6869 7301 000b 4c54 6573 le...this...LTes
00000060: 742f 7465 7374 3b01 000a 536f 7572 6365 t/test;...Source
00000070: 4669 6c65 0100 0974 6573 742e 6a61 7661 File...test.java
00000080: 0c00 0400 0501 0009 5465 7374 2f74 6573 ........Test/tes
00000090: 7401 0010 6a61 7661 2f6c 616e 672f 4f62 t...java/lang/Ob
000000a0: 6a65 6374 0021 0002 0003 0000 0000 0001 ject.!..........
000000b0: 0001 0004 0005 0001 0006 0000 002f 0001 ............./..
000000c0: 0001 0000 0005 2ab7 0001 b100 0000 0200 ......*.........
000000d0: 0700 0000 0600 0100 0000 0400 0800 0000 ................
000000e0: 0c00 0100 0000 0500 0900 0a00 0000 0100 ................
000000f0: 0b00 0000 0200 0c .......

第一行cafe babe 被称为“魔数”,是JVM识别.class文件到标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 8950 4e47

2. 类加载

类加载分为三个步骤:加载,连接,初始化。

2.1 加载:

  • 类加载指的是将class文件读入内存,并为之创建一个java.lang.Class 对象,即程序中使用的任何类时,系统都会为之建立一个java.lang.Class 对象。系统中所有类都是由这个对象实现的。
  • 类的加载由类加载器完成,JVM提供的类加载器叫做系统类加载器(System Class-Loader),此外还可以通过继承ClassLoader基类来自定义类加载器。
  • 通常可以用下面几种方式加载类的二进制数据:
    1. 从本地文件系统加载class文件。
    2. 从jar包中加载class文件,如jar包的数据库启驱动类。
    3. 通过网络加载class文件。
    4. 把一个Java源文件动态编译并执行加载。

2.2 连接

连接阶段负责把类的二进制数据合并到JRE中,其有可分为如下三个阶段:

  1. 验证:确保加载到类信息符合JVM规范,无安全问题。
  2. 准备:为类的静态Field分配内存,并设置初始值。
  3. 解析:将类的二进制数据中的符号引用替换成直接引用。

2.3 初始化

  • 该阶段主要是对静态Field进行初始化,在Java类中对静态Field指定初始值有两种方式:

    1. 声明时即指定初始值,如 staric int a = 5;
    2. 用静态代码块为静态Field指定初始值,如static { b = 5;}
  • JVM初始化一个类包含如下步骤:

    1. 如果这个类还没被加载或连接,则程序先加载并连接该类。
    2. 如果该类的直接父类还没被初始化,则先初始化其父类。
    3. 如果类中有初始化语句,则系统一次执行这些初始化语句。

    所以JVM总是最先初始化java.lang.Object2333333

  • 类初始化的时机(对类进行主动引用时)

    1. 创建类的实例时(new,反射,反序列化)
    2. 调用某个类的静态方法时。
    3. 使用某个类或接口的静态Field或对该Field赋值时
    4. 使用反射来强制创建某个类或接口对应的java.lang.Class对象,如Class.forName("Person")
    5. 初始化某个类的子类时,此时该子类的所有父类都会被初始化。
    6. 直接使用java.exe运行某个主类时。

3. 类加载器及加载机制

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(两个类的 Class 对象不 equals)。

Java类加载器可分为三种:

  1. 根(又叫启动,引导)类加载器(Bootstrap Class-Loader)

    它负责加载Java核心类(比如String,System类)。它比较特殊,因为它是由原生C++实现的,并不是java.lang.ClassLoader 的子类,所以下面运行为null。

    1
    2
    3
    4
    5
    public class TestJdkCl {
    public static void main(String[] args) {
    System.out.println(String.class.getClassLoader());
    }
    }
  2. 扩展类加载器(Extension Class-Loader),加载jre/lib/ext 包下面的jar文件。我们可以通过把自己开发的类打包成JAR文件放入扩展目录来为Java扩展核心类以外的新功能。

  3. 应用(系统)类加载器(Application or System Class-Loader),根据程序的类路径(classpath)来加载Java类。

    它负责在JVM启动时加载来自Java命令的-classpath选项,java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器:

例子:

1
2
3
4
5
6
7
8
9
10
11
public class Test {

public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}

}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

这段代码的输出结果如下:

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@3d4eac69
jdk.internal.loader.ClassLoaders$PlatformClassLoader@38af3868

第一行输出为 Test 的类加载器,即应用类加载器,它是 jdk.internal.loader.ClassLoaders$AppClassLoader 类的实例;第二行输出为启动类加载器,是 jdk.internal.loader.ClassLoaders$PlatformClassLoader@38af3868 类的实例。

3.1 类加载机制

Java类加载机制主要由以下三种:

  • 双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给父类加载器去完成,依次递归,一直到最顶层的启动类加载器;只有在父类加载器无法完成类的加载工作时,当前类加载器才会自己去加载这个类。(注意:类加载器中的父子关系并不是类继承上的父子关系,而是类加载器实例之间的关系)
  • 全盘负责:当一个类加载器加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器载入,除非显式的使用另外一个类加载器来载入。
  • 缓存机制:缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个类时,类加载器先从缓冲区搜索该类,若搜寻不到将读取该类的二进制数据并转换成Class对象存入缓冲区中。这就是为什么修改了Class后需重启JVM才能生效的原因。

JAVA9的改变

\

可见,在JDK 9中,应用程序类加载器可以委托给平台类加载器以及引导类加载器;平台类加载器可以委托给引导类加载器和应用程序类加载器。

此外,JDK 9不再支持扩展机制。 但是,它将扩展类加载器保留在名为平台类加载器的新名称下。 ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。

在JDK 9之前,扩展类加载器和应用程序类加载器都是java.net.URLClassLoader类的一个实例。 而在JDK 9中,平台类加载器(以前的扩展类加载器)和应用程序类加载器是内部JDK类的实例。 如果你的代码依赖于URLClassLoader类的特定方法,代码可能会在JDK 9中崩溃。

JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。

JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。

JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。

  • 当应用程序类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果有合适的模块定义在这些类加载器中,则该类加载器将加载类,这意味着应用程序类加载器现在可以委托给引导类加载器和平台类加载器。 如果在为这些类加载器定义的命名模块中找不到类,则应用程序类加载器将委托给其父类,即平台类加载器。 如果类尚未加载,则应用程序类加载器将搜索类路径。 如果它在类路径中找到类,它将作为其未命名模块的成员加载该类。 如果在类路径中找不到类,则抛出ClassNotFoundException异常。
  • 当平台类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果一个合适的模块被定义为这些类加载器中,则该类加载器加载该类。 这意味着平台类加载器可以委托给引导类加载器以及应用程序类加载器。 如果在为这些类加载器定义的命名模块中找不到一个类,那么平台类加载器将委托给它的父类,即引导类加载器。
  • 当引导类加载器需要加载一个类时,它会搜索自己的命名模块列表。 如果找不到类,它将通过命令行选项-Xbootclasspath/a指定的文件和目录列表进行搜索。 如果它在引导类路径上找到一个类,它将作为其未命名模块的成员加载该类。
0%