前面我们知道类加载有系统自带的 3 种加载器, 也有自定义的加载器, 那么这些加载器之间的关系是什么, 已经在加载类的时候, 谁去加载呢? 这节, 我们将进行讲解
一双亲委派机制
JVM 的 ClassLoader 采用的是树形结构, 除了 BootstrapClassLoader 以外? 每个 ClassLoader 都会有一个 parentClassLoader, 用户自定义的 ClassLoader 默认的 parentClassLoader 是 SystemClassLoader, 当然你可以自己指定需要用哪一个 ClassLoader 的实例, 我们来看他的 API:
默认的无参构造方法使用的是 SystemClassLoader, 你可以通过传入一个 ClassLoader 的实例来指定他的父类加载器这里强调一点, 很多人认为各个父子类加载器之间是继承关系, 这里澄清一下, 父子类加载器之间是组合关系, 子类类加载器会含有一个 parentClassLoader 的对象, 类加载的时候通常会按照树形结构的原则来进行, 也就是说, 首先是从 parentClassLoader 中尝试进行加载, 当 parent 无法进行加载时, 再从当前的类加载器进行加载, 以此类推 JVM 会保证一个类在同一个 ClassLoader 中只会被加载一次
ClassLoader 抽象类为我们定义了一系列的关键的方法, 下来让我们来看一下
1loadClass 方法, 此方法用来加载指定名字的类, ClassLoader 会先从已加载的类中寻找, 如果没有, 则使用父加载器进行加载, 如果加载成功则加载, 否则从当前的类加载器中进行加载, 如果还没有找到该类的 class 文件则会抛出异常 ClassNotFoundException
如果该类需要链接, 则通过 resolveClass 进行链接
2defineClass, 此方法用来将二进制的字节码转换为 Class 对象, 这个对类的自定义加载非常重要, 当然前文我们已经说了, 当类的二进制文件被加载到内存之后, 要进行语法分析, 语义分析等一系列的验证, 如果不符合 JVM 规范, 则抛出 ClassFormateError 错误, 如果生成的类名和字节码中的不一致, 则抛出 NoClassDefFoundException, 如果加载的 class 是受保护的采用不同的标签名的, 或者一 java.* 开头的, 则抛出 SecurityException, 如果要加载的 class 在之前已经被加载过, 则直接抛出 LinkageError
3resolveClass, 此方法完成 Class 的链接, 如果链接过则直接返回当 Java 开发人员调用 Class.forName 来获取一个 class 对象的时候, JVM 会从方法栈上寻找第一个 ClassLoader, 通常也就是执行 Class.forName 的 ClassLoader, 并使用这个 ClassLoader 来加载此类 JVM 为了保护加载执行的类的安全, 不允许 ClassLoader 直接卸载加载了的类, 只有 JVM 才可以卸载, 在 SUN 的 JDK 中, 只有 ClassLoader 没有 被引用的时候, 次 ClassLoader 加载的类才会被卸载!
附: JDK 中 ClassLoader 的部分源码
1 构造函数
- protected ClassLoader(ClassLoader parent) {
- SecurityManager security = System.getSecurityManager();
- if (security != null) {
- security.checkCreateClassLoader();
- }
- this.parent = parent;
- initialized = true;
- }
- protected ClassLoader() {
- SecurityManager security = System.getSecurityManager();
- if (security != null) {
- security.checkCreateClassLoader();
- }
- this.parent = getSystemClassLoader();
- initialized = true;
- }
- 2loadClass
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
- protected synchronized Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
类的这种加载机制我们称之为父委托加载机制, 父委托机制的优点就是能够提高软件系统的安全性因为在此机制下, 用户自定义的类加载器不可能加载本应该由父加载器加载的可靠类, 从而防止不可靠的甚至恶意的代码代替由父类加载器加载的可靠代码如, java.lang.Object 类总是由根类加载器加载的, 其他任何用户自定义的类加载器都不可能加载含有恶意代码的 java.lang.Object 类
被定义的类加载器, 而它的父类加载器则被称为初始类加载器
我们知道 java 中很可能出现类名相同的类, 但是 JVM 却能正常的加载, 是因为我们将相同的类名的类放在了不通的包 (package) 下面, 这个也成为命名空间, 每个类加载器都有自己的命名空间, 命名空间是由该加载器以及所有父加载器所加载的类组成在同一个命名空间中, 不会出现类的完整名字 (包名 + 类名) 相同的两个类; 在不同的命名空间中, 有可能出现类的完整名字相同的两个类
由同一类加载器加载的属于相同包的类组成了运行时包决定两个类是不是属于同一个运行时包, 不仅要看他们的包名称是否相同, 还要看定义类加载器是否相同只有属于同一运行时包的类之间才能相互访问可见 (默认访问级别) 的类和成员假设用户自定义了一个类 java.lang.TestCase 并由用于自定义的类加载器加载, 由于 java.lang.TestCase 和核心类库 java.lang.* 由不同的类加载器加载, 他们属于不同的运行时包, 所以 java.lang.TestCase 不能访问核心库 java.lang 包中的包可见成员
同一个命名空间内的类是相互可见的
子类加载器的命名空间包含所有父类加载器的命名空间, 因此由子类加载器加载的类能看见父类加载器加载的类, 相反, 由父类加载器加载的类不能看见子类加载器加载的类如果两个加载器之间没有直接或者间接的父子关系, 那么他们各自加载的类互不可见
二自定义类加载器
首先类的双亲委派流程为:
首先, 我们定义一个待加载的普通 Java 类: Test.java 放在 com.pony.cl 包下:
- package com.pony.cl;
- public class Test {
- public void hello() {
- System.out.println("恩, 是的, 我是由" + getClass().getClassLoader().getClass()
- + "加载进来的");
- }
- }
注意:
如果你是直接在当前项目里面创建, 待 Test.java 编译后, 请把 Test.class 文件拷贝走, 再将 Test.java 删除因为如果 Test.class 存放在当前项目中, 根据双亲委派模型可知, 会通过
sun.misc.Launcher$AppClassLoader
类加载器加载为了让我们自定义的类加载器加载, 我们把 Test.class 文件放入到其他目录
接下来就是自定义我们的类加载器:
- import java.io.FileInputStream;
- import java.lang.reflect.Method;
- public class Main {
- static class MyClassLoader extends ClassLoader {
- private String classPath;
- public MyClassLoader(String classPath) {
- this.classPath = classPath;
- }
- private byte[] loadByte(String name) throws Exception {
- name = name.replaceAll("\\.", "/");
- FileInputStream fis = new FileInputStream(classPath + "/" + name
- + ".class");
- int len = fis.available();
- byte[] data = new byte[len];
- fis.read(data);
- fis.close();
- return data;
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- try {
- byte[] data = loadByte(name);
- return defineClass(name, data, 0, data.length);
- } catch (Exception e) {
- e.printStackTrace();
- throw new ClassNotFoundException();
- }
- }
- };
- public static void main(String args[]) throws Exception {
- MyClassLoader classLoader = new MyClassLoader("D:/test");
- Class clazz = classLoader.loadClass("com.pony.cl.Test");
- Object obj = clazz.newInstance();
- Method helloMethod = clazz.getDeclaredMethod("hello", null);
- helloMethod.invoke(obj, null);
- }
- }
注意点:
Object obj = clazz.newInstance();
不能写成:
Test obj = (Test)clazz.newInstance();
如果写成这样会报错, 因为当前的这个类是由系统加载器加载, 而 Test 是由自定义加载器加载, 那么系统类加载和自定义类的加载器不属于同一个运行时包, 这个时候是没有办法直接转换的, 只能通过反射的方式去访问, 反射是唯一一种可以跨越在不同运行时包的方法
参考资料:
圣思园张龙老师深入 Java 虚拟机系列
来源: https://www.cnblogs.com/pony1223/p/8654678.html