Java类加载器详解

类加载器详解

一、类加载器5大部分

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。
加载机制

加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

1
public static int v = 8080;

实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器<clinit>方法之中,这里我们后面会解释。
但是注意如果声明为:

1
public static final int v = 8080;

在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

1
2
3
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info

等类型的常量。
下面我们解释一下符号引用和直接引用的概念:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>方法的过程。<clinit>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<clinit>方法执行之前,父类的<clinit>方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>()方法。(clinit初始化是对类成员信息进行初始化,init是对对象实例进行初始化操作)
注意以下几种情况不会执行类初始化:
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
定义对象数组,不会触发该类的初始化。
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
通过类名获取Class对象,不会触发类的初始化。
通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

二、类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib(JRE/lib/rt.jar) 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext(JRE/lib/ext/*.jar) 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
  4. 自定义的类加载器(User ClassLoader):负责加载用户自定义的路径上的类库。
    JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

委托图
类加载器的委托机制:

  1. 首先当前线程去加载线程中的第一个类
  2. 如果A中引用了B,java虚拟机将使用加载类A的类加载取来加载B
  3. 还可以直接使用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。

每个类加载器加载类时,又先委托给其上级类加载器。

  1. 当所有祖先类加载器没有加载到类,回到发起者类加载器,还加载不了就抛出异常ClassNotFoundException,不是再去找发起类加载器的儿子节点,因为没有getChild方法。

eg.比如说我们自定义一个类A,当我们去获取当前类加载器的名称时,肯定是AppClassLoader加载器,因为我们当前类只有CLASSPATH路径下有没如果我们将当前的项目打包成test.jar将jar包放入到JRE/lib/ext路径下,这时候我们在CLASSPATH中类就不会被加载,首先加载的是ext下的类A。这时候我们输出类A加载器名称的时就会输出ExtClassLoader。下面做演示。

1
2
3
public static void main(String[] args) {
System.out.printf(ClassLoaderTest.class.getClassLoader().getClass().getName());
}

ClassLoaderTest是我们新建立的类,获取当前类的加载器时输出内容为:

1
sun.misc.Launcher$AppClassLoader

如果打包成jar包放到,ext文件下我们来看一下内容:
ext目录结构

这是输出一下内容查看一下:

1
sun.misc.Launcher$ExtClassLoader

1
2
3
4
5
6
ClassLoader classLoader = com.classloadertest.ClassLoaderTest.class.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader.getClass().getName());
classLoader=classLoader.getParent();
}
System.out.println(classLoader);

循环输出类加载器结构的时候会出现

1
2
sun.misc.Launcher$ExtClassLoader
null

因为Bootstrap类加载器实现不是由Java实现所以获取不到字节码,也就是为null的时候意味着是树状结构的顶层。

三、自定义类加载器

有些时候我们需要实现自定义的类加载器来加载一些类,这时候我们可以继承ClassLoader来进行实现自定义的类加载器。我们先看一下jdk中的ClassLoader的源码实现:

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
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. 首先检测当前类有没有加载,如果加载在就不需要加载直接返回字节码
  2. 如果没有被加载,这时候会委托加载机制,通过递归从父加载器开始找,直到父类加载器为bootstrap ClassLoader为止,如果父类加载器找到了该字节码后没直接返回如果没有就返回让子类加载器加载,当子类加载器时,
  3. 如果还没有找到,这时候调用findClass方法进行查找。
  4. 最后根据resolve的值,判断这个class是否需要解析。
    流程图

首先类加载器进行加载时当前线程的上线文的ClassLoader也就是APPClassLoader,当加载类时调用LoadClass方法,当调用的时候会检测当前类有没有被加载如果被加载了,就不需要在加载了直接返回,反之调用父类加载器的LoadClass方法,当父类加载器的父加载器不是null的时候就再去调用LoadClass方法,这时候的类加载器是ExtClassLoader加载器,父类加载是BootStrap ClassLoader,这时候继续递归调用LoadClass方法,这时候类加载器为BootStrap ClassLoader, 这时候就不需要再往下寻找了, parent ClassLoader为null。这是调用findBootstrapClassOrNull(name)方法,如果c不为null,这时候就直接返回,但是如果为null就调用findClass进行查询加载,如果还为null,这时候就会调用父类加载器ExtClassLoaderfindClass方法,如果还是没有找到,则调用线程本身的类加载器调用findClass方法返回相应字节码,如果还没有找到抛出异常。

1
类加载器采用了设计模式是模板方法,也就是LoadClass方法是不需要进行改变的,我们需要关注的就是findClass()方法就好了。所以我们写自己的ClassLoader只需要继承ClassLoader并重写findClass方法即可。

参考地址:
https://www.ziwenxie.site/2017/06/07/java-jvm-classloader/
http://blog.csdn.net/briblue/article/details/54973413

文章目录
  1. 1. 类加载器详解
    1. 1.1. 一、类加载器5大部分
      1. 1.1.1. 加载
      2. 1.1.2. 验证
      3. 1.1.3. 准备
      4. 1.1.4. 解析
      5. 1.1.5. 初始化
    2. 1.2. 二、类加载器
    3. 1.3. 三、自定义类加载器