前言

一个class文件到底是如何加载的呢,本文我们来具体研究下类加载的过程。

类加载子系统

  1. 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。
  2. 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
  3. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
  4. 如果调用构造器实例化对象,则该对象存放在堆区;

类加载器的作用

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建 一个java.lang.Class对象,用来封装类在方法区内的数据结构。

注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个 程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

  1. class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM当中来根据这个文件实例化出n个一模一样的实例。

  2. class file 加载到JVM中,被称为DNA元数据模板。

  3. 在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一 个快递员的角色。

类加载器分类

  1. jvm支持两种类型的加载器,分别是引导类加载器自定义加载器
  2. 引导类加载器是由c/c++实现的,自定义加载器是由java实现的;
  3. jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器;
  4. 按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)

几种类加载器的从上而下的关系如下图所示:

启动类加载器(Bootstrap Class Loader)

  1. 这个类加载器使用c/c++实现,嵌套在jvm内部;
  2. 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  3. 并不继承自java.lang.ClassLoader,没有父加载器。

扩展类加载器(Extension Class Loader)

  1. java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  2. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。
  3. 父类加载器为启动类加载器

系统类加载器(System Class Loader)

  1. java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现 ;
  2. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader ;
  3. 父类加载器为扩展类加载器;
  4. 通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

测试类加载器的代码如下:

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
package com.example.test;

public class ClassLoaderTest {

public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2


//获取系统类加载器的父类加载器,扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@7ba4f24f

//获取系统类加载器的上层启动类加载器,这里会获取不到
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); //null


//获取用户自定义类的加载器 classLoader的打印结果和systemClassLoader的结果完全一致,默认使用系统类加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2

//核心类库使用的是启动类加载器,比如String
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader); //null
}

}

运行结果:

1
2
3
4
5
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7ba4f24f
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

自定义类加载器

在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的 加载方式。

双亲委派模型

什么是双亲委派模型

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个 请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时 (即 ClassNotFoundException ),子加载器才会尝试自己去加载。

为什么需要双亲委派模型

为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:

黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数 稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且 通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么 JVM 就可能误以为黑客自定义的 java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加 载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。 或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就 好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较 的类型的类加载器不同,那么会返回false。

举个例子:

ClassLoader1 、 ClassLoader2 都加载 java.lang.String 类,对应Class1、Class2对象。那么 Class1 对象不属于 ClassLoad2 对象加载的 java.lang.String 类型。

总结

我们认识了类加载器的作用,以及常见的几种类加载器(启动类加载器、扩展类加载器、系统类加载器、自定义类加载器),最后认识了双亲委派模型。