「深入理解 JVM 一」类加载器及自定义类加载器
一、 类加载器介绍
类加载器负责在运行期间将 Java 类动态加载到 JVM 内存中。因此 JVM 不需要了解底层文件或者文件系统来运行 Java 程序。类经过: 加载、链接(验证、准备、解析)、初始化,最终形成可以被虚拟机直接使用的 Java 类型。
-
加载:将 .class 文件加载到内存中
-
链接:
- 验证:验证 class 文件的正确性
- 准备:给类的静态变量分配内存,并且赋默认值。
- 解析:将符号引用变为直接引用 ,类加载的 resolve() 方法。相当于将另一个被引用的类的方法或者成员变量解析为直接定位到该方法或者成员变量的地址,访问的目标被加载到内存中。
-
初始化:对类的静态变量赋值
二、类加载器的种类
2.1 根加载器 (BootStrap ClassLoader)
主要加载 JDK 的内部类,主要是 rt.jar 以及其他在 */jre/lib 下的核心类。根加载器是所有加载器的父加载器。
2.2 扩展类加载器 (Extension ClassLoader)
是根加载器的子加载器,负责 $JAVA_HOME/lib/ext 目录下或者 java.ext.dirs 系统变量指定路径下扩展类的加载。
2.3 系统类加载器 (Application ClassLoader)
也叫做应用程序类加载器。是扩展类加载器的子加载器。负责加载用户类路径 classpath 指定的类。
三、类加载的过程
当 JVM 需要一个类时,类加载器收到类加载请求。首先从下往上检索该类是否被加载过,如果被加载过那么直接返回。否则,该加载器先递归地委托给父加载器加载,效果也就是自顶向下加载类,如果父加载器不能加载类那么就交给子加载器加载。如果父加载器加载失败,那么抛出 ClassNotFoundException 异常,再调用自己的 findClass() 方法进行加载。
3.1 双亲委派模型 (delegation)
以上过程就是双亲委派的过程。
为什么使用双亲委派模型呢?
- 提高安全性
- 为了保护系统核心类不被篡改。如果用户编写了一个 java.lang.Object 这种核心类,功能和系统 Object 类相同,但是植入了恶意代码。有了双亲委派模型,自定义的 Object 类是不会被加载的,因为根加载器会首先加载系统 Object 类,而不会加载自定义的 Object 类。
- 防止程序混乱
- 首先明确,jvm判定两个对象同属于一个类型:同名类实例化,实例对应的同名类的加载器必须相同。
- 要是每个加载器都自己加载的话,那么可能会出现多个 Object 类,导致混乱。
四、自定义类加载器
4.1 自定义类加载器的实现
自定义类加载器需要继承 ClassLoader 类,然后重写 findClass() 方法 或者重写 loadClass() 方法(如果需要打破双亲委派模型)
下面我们重写 findClass() 方法:
import java.io.*;
public class MyTest16 extends ClassLoader{
private String classLoaderName;
private String extensionName = ".class";
public MyTest16(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public MyTest16(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
private byte[] loadClassData(String name){
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
byte[] data = null;
try {
name = name.replace('.', '/');
// class 文件绝对路径
String sources = "/home/jason/Documents/Programs/JVM/target/classes/" + name + this.extensionName;
File file = new File(sources);
fis = new FileInputStream(file);
int len = fis.available();
data = new byte[len];
fis.read(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
public String toString() {
return "[" + this.classLoaderName +"]";
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = this.loadClassData(name);
return defineClass(name, b, 0, b.length);
}
public static void main(String[] args) throws Exception{
// 这里使用 ClassLoader.getSystemgetSystemClassLoader().getParent() 得到 扩展类加载器,
// 这样扩展类加载器就加载不到我们定义的类,因而使用我们自定义的类加载器加载。
// 也可以将 classpath 下生成的 .class 文件移动到非 classpath 路径下,这样系统类就不能加载指定类了
ClassLoader classLoader = new MyTest16(ClassLoader.getSystemClassLoader().getParent(),"lyClassLoader");
Class<?> clazz = Class.forName("com.ly.test.Test1", true, classLoader);
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
}
}
输出结果:
MyTest13@154617c
[lyClassLoader]
这里 findClass 内部需要加载字节码文件的字节数组,我们写了一个 loadClassData() 方法来实现这个功能。
需要注意的是:
- 需要修改 sources 的前缀路径,来定位我们要加载的 .class 文件
- 要想得到输出的第二行结果:
- main() 方法下 新建 MyTest16 对象时,第一个参数使用ClassLoaderget.getSystemClassLoader().getParent() 得到 扩展类加载器,这样扩展类加载器就加载不到我们定义的类,因而使用我们自定义的类加载器加载。
- 也可以将 classpath 下生成的 .class 文件移动到非 classpath 路径下,这样系统类就不能加载指定类了
4.2 自定义类加载器的应用
- 可以通过使用自定义类加载器来加载网络上的 class 文件。也可以方便地对加载的类库进行隔离。
- 可以修改字节码文件,然后通过自定义类加载器来加载
- 可以实现版本机制,通过加载拥有相同名称内容不同的字节码文件和包。
五、源码解析
5.1 loadClass() 解析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否被加载过
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) {
// 如果使用内置的类加载器不能加载类,那么就会调用 findClass 方法来加载类,findClass 方法要求重写,否则会直接抛出 ClassNotFoundException
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;
}
}
- 首先检查类是否被加载过,这个过程是自底向上的。
- 如果类没有被加载过那么就自顶向下使用类加载器来加载。
- 如果系统内置类加载器都不能加载,那么调用 findClass()方法,使用自定义类加载器来加载。
流程图:
5.2 findClass()
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
我们可以看到内置 findClass() 是直接抛出异常的,如果非要使用自定义类加载器,该方法必须要重写。
5.3 defineClass()
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
在 findClass() 内调用 defineClass() 方法,将字节数组转换成类的实例。在我们使用前,需要解析它。
六、参考
[2. Java类加载器及自定义](https://segmentfault.com/a/1190000012925715)
3. 周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社