ClassLoader类加载过程浅析

类加载概述

概述

定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。类加载和连接的过程都是在运行期间完成的。

类加载分为 加载(Loading)验证(Verfication)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading) 这 7 个阶段。

  • 验证、准备、解析 被称为 连接(Linking)
  • 解析阶段有可能在初始化之前,有可能在初始化之后(动态绑定)。

类加载的过程

加载

加载阶段的工作

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,做为方法区这些数据的访问入口。

加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。

验证

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,并能被当前版本的虚拟机处理;
  • 元数据验证:对字节码描述信息进行语义分析,保证其描述信息符合java语言规范;
  • 字节码验证:对类方法体进行数据流和控制流分析,保证类的方法在运行时不会做出危害虚拟机的行为;
  • 符号引用验证:发生在将符号引用转化为直接引用的时候,在解析阶段中发生。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

注意下面两种类变量的初始化结果:

1
2
private static final int a = 1;
private static int b = 2;

准备阶段结束后,类变量 b 的值为0,而 a 的值为1。这是因为 static final 常量在编译期就将其结果放入了调用它的类的常量池中。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。两种方式:

  • 声明类变量时指定初始值;
  • 使用静态代码块为类变量指定初始值。

双亲委托模式

定义:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

java.lang.ClassLoader的loadClass() 方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

上面代码片段的逻辑如下:

  1. 执行 findLoadedClass(String) 去检测这个class是不是已经加载过了。
  2. 执行父加载器的 loadClass() 方法。如果父加载器为 null,则 JVM 内置的加载器去替代,也就是 Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为 null,但仍然说 Bootstrap ClassLoader 是它的父加载器。
  3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

如果class在上面的步骤中找到了,参数 resolve 又是true的话,那么 loadClass() 又会调用resolveClass(Class) 这个方法来生成最终的Class对象。


如何打破双亲委派机制?思路有如下几种:

  1. 重写 loadClass() 方法,从机制上破坏双亲委派;
  2. 使用线程上下文加载器,如 setContextClassLoader(ClassLoader cl) 设置线程的上下文类加载器为自定义类加载器。

类加载注意事项

  1. ExtClassLoader 的父 LoaderBootstrap Loader(引
    导类加载器)是用 C 语言实现的,因此 通过 Loader.getParent() 会返回 null

  2. JVM 中一个类用其全名和一个加载类 ClassLoader 的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。同时,不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载

  3. Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

  4. 当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。如果用户自定义的类加载器将父类加载器强制设置为 null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器。

  5. 自定义类加载器时,一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑。在 JVM 规范和 JDK 文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。

  6. 默认的线程上下文类加载器是 系统类加载器(AppClassLoader)。使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.

  7. 类加载器与Web容器:以 Apache Tomcat 为例,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。

---(完)---
Yves wechat
扫一扫互相关注吧~
  • 本文作者: Yves
  • 本文标题: ClassLoader类加载过程浅析
  • 发布时间: 2018年03月05日 - 10:03
  • 更新时间: 2020年07月22日 - 00:07
  • 本文链接: /2018/03/05/classloader_overview/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

扫一扫关注公众号