跳到主要内容

21.3 Java类加载器

1. 三种类加载器

1.1 类的加载

当程序要使用某个类的时候,如果该类还未被加载进Jvm的内存,则系统会通过

加载、连接、初始化
三个步骤来实现对类的初始化。

  • 加载
    • 是指将class文件以二进制流读进Jvm内存的方法区,并在内存的堆区创建一个Class对象,指向方法区的class数据;
    • 任何类被使用时,系统都会为其创建Class对象,且一个类只有一个Class对象。
  • 连接
    • 验证,是否有正确的内部结构,并和其他类协调一致(这也是确保Jvm的运行安全);
    • 准备,负责为类的静态成员分配内存,并设置默认的初始化值;
    • 解析,将类的二进制数据中的符号引用替换为直接引用。
  • 初始化
    • 为静态成员赋值(实际值)。

加载、验证、准备、初始化这4个阶段的顺序是确定的,而解析阶段在某些情况先会在初始化阶段之后再开始。

1.2 类加载流程图示

1.3 类的加载时机

  • 创建类的实例时;
  • 创建子类实例时,会先加载父类;
  • 访问类的静态成员时(静态属性、静态方法,被final修饰、已在编译期把结果放入常量池的静态字段除外);
  • 使用反射机制创建类或接口对应的java.lang.Class对象;
  • 使用java.exe命令运行某个主类。

1.4 类加载器

类加载器的作用:

  • 负责将class文件加载进内存,并为其生产Class对象

三种类加载器:

  • Bootstrap ClassLoader 根类加载器
  • Extension ClassLoader 扩展类加载器
  • System ClassLoader 系统类加载器

· Bootstrap ClassLoader

根类加载器,也被称为“引导类加载器”,负责Java核心类库的加载,这些类库在Jdk中的jre/lib目录下的rt.jar中。如SystemString等。

· Extension ClassLoader

扩展类加载器,用来加载JDK中的jre/lib/ext目录下的全部jar包。

· System ClassLoader

系统类加载器,有的文档也称它为"Application ClassLoader"应用类加载器。

负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径,自定义的类均有系统类加载器加载。

2. 双亲委派机制

双亲委派机制的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。下面举一个大家都知道的例子说明为什么要使用双亲委派模型。

黑客自定义一个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。

3. 自定义类加载器

提示

可以通过继承 ClassLoader 这个抽象类来自定义类加载器,自定义的类加载器的父加载器是系统类加载器。

3.1 ClassLoader 类

ClassLoader 是一个抽象类,所有的类加载器(除了根类加载器,根类加载器是在Jvm中,通过C++编写的)都继承自这个类。

下面介绍 ClassLoader 中的4个主要方法。

3.1.1 loadClass()

这个方法的作用是确认要加载的类是否已经被加载,以及双亲委派机制的实现。

该方法会先调用 findLoadedClass(name) 方法确认要加载的类是否已经被加载,因为一个类只会被加载一次。findLoadedClass(name) 内调用native方法来查询类是否被加载(native方法指非Java实现的代码,一般通过C、C++实现)。

如果没有被加载,则会调用父加载器的 loadClass() 方法,委托父加载器去加载类,这里就是双亲委派的具体实现。如果父加载器无法加载,则最终会由自己去调用 findClass() 方法加载类。

源代码:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 确认类是否已经被加载(调用native方法)
Class<?> c = findLoadedClass(name);
// 如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父加载器不为null,调用父加载器的 loadClass() 方法加载类
c = parent.loadClass(name, false);
} else {
// 如果父加载器为null,调用虚拟机的根类加载器加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 如果父加载器无法加载类,则调用自己的 findClass() 方法加载类
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;
}
}
提示

在自定义类加载器时,一般不会重写 loadClass() 方法。如果要打破双亲委派,自定义类加载实现时可以重写该方法。

3.1.2 findClass()

这个方法是在 loadClass() 方法中被调用的,用来通过类的二进制名查找类。在自定义类加载器时重写该方法来实现不同功能的类加载功能。(参考下面 3.2、3.3 的代码示例)

源代码:

 protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

3.1.3 defineClass()

该方法是将一个字节数组转换成Class实例。一般在自定义类加载器时,我们会重写 ClassLoader 的 findClass() 方法,通过类的二进制名查找要加载类并将其转换成字节数组,在调用 defineClass() 生成 Class 实例。

defineClass() 方法最终是通过调用native方法来生成 Class 实例的。

源代码:

   protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

3.1.4 resolveClass()

该方法的作用是连接类,这里的连接指的就是上面提到的类加载过程中的连接 Linking。该方法调用native方法实现。

源代码:

 protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}

3.2 示例:自定义文件类加载器

import java.io.*;

/**
* 自定义的文件类加载器,加载磁盘中的class文件
*
* @author liyan
* @since 2021-10-13 17:46
*/
public class MyFileClassLoader extends ClassLoader {

private String dir;

public MyFileClassLoader(String dir) {
this.dir = dir;
}

public MyFileClassLoader(String dir, ClassLoader parent) {
super(parent);
this.dir = dir;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream in = null;
ByteArrayOutputStream baos = null;
try {
// 全限定类型转成文件的全路径
String file = dir + File.separator + name.replace('.', File.separatorChar).concat(".class");
// 文件输入流,将class文件写入内存
in = new FileInputStream(file);
// 字节数组输出流
baos = new ByteArrayOutputStream();
// 1KB字节缓冲区
byte[] buf = new byte[1024];
int len = -1;
// 读取文件以字节的形式写入缓冲区
while ((len = in.read(buf)) != -1) {
// 将缓冲区的字节数据写到字节输出流中
baos.write(buf, 0, len);
}
// 输出流转成字节数据
byte[] data = baos.toByteArray();
// 生成Class实例
return defineClass(name, data, 0, data.length);
} catch (FileNotFoundException e) {
throw new ClassNotFoundException(name);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
// 关闭流资源
if (in != null) {
in.close();
}
if (baos != null) {
baos.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

3.3 示例:自定义URL类加载器

import java.io.*;
import java.net.URL;

/**
* 自定义的网络类加载器,加载网络资源中的class文件
*
* @author liyan
* @since 2021-10-14
*/
public class MyURLClassLoader extends ClassLoader {

private String url;

public MyURLClassLoader(String url) {
this.url = url;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream in = null;
ByteArrayOutputStream baos = null;
try {
// 网络资源路径
String path = url + name.replace('.', '/').concat(".class");
// 创建URL实例
URL url = new URL(path);
// 拿到输入流
in = url.openStream();
// 字节数组输出流
baos = new ByteArrayOutputStream();
// 1KB字节缓冲区
byte[] buf = new byte[1024];
int len = -1;
// 读取文件以字节的形式写入缓冲区
while ((len = in.read(buf)) != -1) {
// 将缓冲区的字节数据写到字节输出流中
baos.write(buf, 0, len);
}
// 输出流转成字节数据
byte[] data = baos.toByteArray();
// 生成Class实例
return defineClass(name, data, 0, data.length);
} catch (FileNotFoundException e) {
throw new ClassNotFoundException(name);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
// 关闭流资源
if (in != null) {
in.close();
}
if (baos != null) {
baos.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}