java的类加载器以及如何自定义类加载器

ClassLoader作用

  • 类加载流程的"加载"阶段是由类加载器完成的。

类加载器结构

结构:BootstrapClassLoader(祖父)–>ExtClassLoader(爷爷)–>AppClassLoader(也称为SystemClassLoader)(爸爸)–>自定义类加载器(儿子)

关系:看括号中的排位;彼此相邻的两个为父子关系,前为父,后为子

注意,这里的父子关系并不是通过继承构建的,而是在创建子加载器时,将父加载器通过setParent设置进去的,也就是组合模式,而非继承模式

BootstrapClassLoader

  • 下边简称为boot
  • C++编写
  • 为ExtClassLoader的父类,但是通过ExtClassLoader的getParent()获取到的是null(在类加载器部分:null就是指boot)
  • 主要加载:\Java\jdk1.6\jre\lib*.jar(最重要的就是:rt.jar)

ExtClassLoader:

  • 下边简称为ext
  • java编写,位于sun.misc包下,该包在你导入源代码的时候是没有的,需要重新去下
  • 主要加载:\Java\jdk1.6\jre\lib\ext*.jar(eg.dnsns.jar)

AppClassLoader:

  • 下边简称为app
  • java编写,位于sun.misc包下
  • 主要加载:类路径下的jar,也就是classpath下的类,包括编译后的classes文件夹下的class文件和jar包中的class文件

自定义类加载器:

  • 下边简称为custom,自定义加载器的parent为AppClassLoader
  • 自己编写的类加载器,需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String
    name)方法,若想打破双亲委托机制,需要重写loadClass方法
  • 主要加载:自己指定路径的class文件 类加载器之间的关系见下图
    java的类加载器以及如何自定义类加载器_第1张图片

不同类加载器的命名空间关系

咱们先回顾一下命名空间的概念:

  • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。(请结合下图一起看,想明白)
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

得出命名空间的关系如下:(请结合下图一起看,想明白)

  • 同一个命名空间的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。
    java的类加载器以及如何自定义类加载器_第2张图片
    这样,子加载器的命名空间,包含了父加载器的命名空间,就可以保证,子加载器加载的类,可以使用父加载器加载的类。也就是说,父加载器加载的类,对子加载器可见,同级别加载器加载的类,不可见

类的唯一性

在运行期,一个类的唯一性是由以下2点共同决定:

  • 该类的完全限定名(binary name)。(包+类名)
  • 用于加载该类的[定义类加载器],即defining class loader。
    上述2点都一样,才代表该类(可以理解为该类的Class对象)是一样的。
    如果同样的名字,不同的类加载器加载,那么这2个类是不一样的。即使.class文件完全一样,.class文件路径一样,这2个类也是不一样的。

双亲委托机制

这也是类加载器加载一个类的整个过程。

过程:假设我现在从类路径下加载一个类A,

1)那么app会先查找是否加载过A,若有,直接返回;

2)若没有,去ext检查是否加载过A,若有,直接返回;

3)若没有,去boot检查是否加载过A,若有,直接返回;

4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib*.jar下找到了指定名称的类,则加载,结束;

5)若没找到,boot加载失败;

6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext*.jar下找到了指定名称的类,则加载,结束;

7)若没找到,ext加载失败;

8)app加载器加载,若在类路径(classpath)下找到了指定名称的类,则加载,结束;

9)若没有找到,抛出异常ClassNotFoundException

注意:

  • 在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程 ("解析"见 第四章 类加载机制)
  • 类的加载过程只有向上的双亲委托,没有向下的查询和加载,假设是ext在E:\Java\jdk1.6\jre\lib\ext*.jar下加载一个类,那么整个查询与加载的过程与app无关。
  • 假设A加载成功了,那么该类就会缓存在当前的类加载器实例对象C中,key是(A,C)(其中A是类的全类名,C是加载A的类加载器对象实例),value是对应的java.lang.Class对象
  • 上述的1)2)3)都是从相应的类加载器实例对象的缓存中进行查找
  • 进行缓存的目的是为了同一个类不被加载两次
  • 使用(A,C)做key是为了隔离类,假设现在有一个类加载器B也加载了A,key为(A,B),则这两个A是不同的A。这种情况怎么发生呢?
  • 假设有custom1、custom2两个自定义类加载器,他们是兄弟关系,同时加载A,这就是有可能的了

总结:

  • 从底向上检查是否加载过指定名称的类;从顶向下加载该类。(在其中任何一个步骤成功之后,都会中止类加载过程)
  • 双亲委托的好处:假设自己编写了一个java.lang.Object类,编译后置于类路径下,此时在系统中就有两个Object类,一个是rt.jar的,一个是类路径下的,在类加载的过程中,当要按照全类名去加载Object类时,根据双亲委托,boot会加载rt.jar下的Object类,这是方法结束,即类路径下的Object类就没有加载了。这样保证了系统中类不混乱。

ClassLoader.java类提供了loadClass方法,确保双亲委派机制,如下


```bash
/**
     * 根据指定的binary name加载class。
     * 步驟:
     * 假设我现在从类路径下加载一个类A,
     * 1)那么app会先查找是否加载过A(findLoadedClass(name)),若有,直接返回;
     * 2)若没有,去ext检查是否加载过A(parent.loadClass(name, false)),若有,直接返回;
     * findBootstrapClassOrNull(name) 3)4)5)都是这个方法
     * 3)若没有,去boot检查是否加载过A,若有,直接返回;
     * 4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib\*.jar下找到了指定名称的类,则加载,结束;
     * 5)若没找到,boot加载失败;
     * findClass(name) 6)7)8)9)都是这个方法
     * 在findClass中调用了defineClass方法,该方法会生成当前类的java.lang.Class对象
     * 6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext\*.jar下找到了指定名称的类,则加载,结束;
     * 7)若没找到,ext加载失败;
     * 8)app加载,若在类路径下找到了指定名称的类,则加载,结束;
     * 9)若没有找到,抛出异常ClassNotFoundException
     * 注意:在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程
     */
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Class c = findLoadedClass(name);//检查要加载的类是不是已经被加载了
        if (c == null) {//没有被加载过
            try {
                if (parent != null) {
                    //如果父加载器不是boot,递归调用loadClass(name, false)
                    c = parent.loadClass(name, false);
                } else {//父加载器是boot
                    /*
                     * 返回一个由boot加载过的类;3)
                     * 若没有,就去试着在E:\Java\jdk1.6\jre\lib\*.jar下查找 4)
                     * 若在bootstrap class loader的查找范围内没有查找到该类,则返回null 5)
                     */
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //父类加载器无法完成加载请求
            }
            if (c == null) {
                //如果父类加载器未找到,再调用本身(这个本身包括ext和app)的findClass(name)来查找类
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
![image-20220114161133272](https://img-blog.csdnimg.cn/ab9197edf4d5416f836b813f29e58500.png)
ClassLoader类提供了findClass方法的空实现,jdk1.2以后,用户如果需要自定义类加载器,只需要继承自ClassLoader,然后覆写findClass方法,这样可以确保双亲委派机制。jdk1.2以前,用户需要重写loadClass方法。

```bash
AppClassLoader(系统加载器)和ExtClassLoader(扩展类加载器)都继承自URLClassLoader,URLClassLoader覆写了findClass方法

说明:

  • 该段代码中引用的大部分方法实质上都是native方法
  • 其中findClass方法的类定义如下:
/**
     * 查找指定binary name的类
     * 该类应该被ClassLoader的实现类重写
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

关于findClass可以查看URLClassLoader.findClass(final String name),其中引用了defineClass方法,在该方法中将二进制字节流转换为了java.lang.Class对象。

递归基于栈实现。

上述的代码如果不清楚递归的意义是看不清的。

解释:

  • app的loadClass()方法执行到ext的loadClass(),这时候对于app_loadClass()中剩余的findClass()会在栈中向下压;
  • 然后执行ext_loadClass(),当执行到findBootstrapClassOrNull(name),这时候ext_loadClass()中剩余的findClass()也会从栈顶向下压,此时ext_loadClass()_findClass()仅仅位于app_loadClass()_findClass()的上方;
  • 然后执行findBootstrapClassOrNull(name),当boot检测过后并且执行完加载后并且没成功,boot方法离开栈顶;
  • 然后执行此时栈顶的ext_loadClass()_findClass()
  • 然后执行此时栈顶的app_loadClass()_findClass()

这样,就完成了双亲委托机制。

实验

  1. 定义pojo类
public class MyPerson {
 
    private MyPerson myPerson;
 
    public void  setMyPerson(Object obj){
        this.myPerson = (MyPerson)obj;
    }
}
  1. 定义自定义类加载器
package com.hisense.testbean.classloader;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked : " + className);
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] data = this.loadClassData(className);

        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        byte[] data = null;
        className = className.replace(".", "/");
        try(InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while(-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

测试1

public class TestClassLoaderNameSpace {
    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
        CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
        String path = "";
        System.out.println(path);
        loader1.setPath(path);
        loader2.setPath(path);
        Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");

        System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
        System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
        System.out.println( clazz1 == clazz2);
 
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(object1, object2);
    }
}

设置path为“”,实际上时设置类的加载路径为classpath,此时根据类的加载机制

第一次调用

loader1.loadClass("com.hisense.testbean.classloader.MyPerson");

CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),发现类属于classpath路径,加载类,通过defineclass完成加载类)

第二次调用

loader2.loadClass("com.hisense.testbean.classloader.MyPerson");

CustomizedClassLoader(未找到)->app(已经加载过,缓存中找到)直接返回加载的类person

因此,person的加载器实际上为AppClassLoader,因此我们不能把com.hisense.testbean.classloader.MyPerson放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载

以上输出为:

clazz1的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
clazz2的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
true

测试2

注意:

需要删除target下的person.class文件,否则会优先通过app加载器,取classpath下加载,自定义的CustomizedClassLoader就不会加载了

public class TestClassLoaderNameSpace {
    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
        CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
        String path = "D:/test/";
        System.out.println(path);
        loader1.setPath(path);
        loader2.setPath(path);
        Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");

        System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
        System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
        System.out.println( clazz1 == clazz2);
 
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(object1, object2);
    }
}

手动把Person.java编译为Person.class,拷贝到"D:/test/“下,设置类的加载路径为"D:/test/”,由于"D:/test/"不在系统classpath路径下,因此加载时,

CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),不是classpath下的路径,不加载,递归栈返回)->CustomizedClassLoader(调用自己的findClass方法,自己取D:/test/下加载类),加载完成,返回类

此时,调用自定义加载器CustomizedClassLoader的findClass加载类,但是由于CustomizedClassLoader不是单例,此处我们定义了两个CustomizedClassLoader的实例loader1和loader2,loader1加载完以后,同级别的loader2并不知道person类的存在,因此loader2会再次加载一次。因此,二者不是同一个class类实例。彼此不可见

输出

D:/test/
findClass invoked : com.hisense.testbean.classloader.MyPerson
class loader name : loader1
findClass invoked : com.hisense.testbean.classloader.MyPerson
class loader name : loader2
clazz1的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@340f438e
clazz2的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@2d6e8792
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.hisense.testbean.classloader.TestClassLoaderNameSpace2.main(TestClassLoaderNameSpace2.java:23)
Caused by: java.lang.ClassCastException: com.hisense.testbean.classloader.MyPerson cannot be cast to com.hisense.testbean.classloader.MyPerson
    at com.hisense.testbean.classloader.MyPerson.setMyPerson(MyPerson.java:8)
    ... 5 more

报错,说明两个person类时不可见的,因为二者时不同加载器加载的

自定义类加载器的父类为什么是AppClassLoader

首先有一个概念要了解,我们通常说在jdk中,默认有三个类加载器,bootstrapClassLoader、ExtClassLoader、AppClassLoader,我们说前者是后者的父类加载器,但是实际上,这里所谓的父类加载器,并不是Java中的父子类继承关系,而是说:

  • AppClassLoader中有一个parentClassLoader设置的值是ExtClassLoader
  • ExtClassLoader中的parentClassLoader设置的是bootstrapClassLoader
    我们自定义的类加载器,对应的parentClassLoader是AppClassLoader
  • 我们如果自定义一个类加载器,默认设置的父类加载器是AppClassLoader,这个原因是在因为在初始化自定义类加载器的时候,会指定其parentClassLoader为AppClassLoader

Launcher
这个类是jre中用来启动main()方法的入口,在这个类中,我们着重关注的是初始化构造方法

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
    	// 初始化extClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 初始化appClassLoader,这里的这个赋值是比较重要的
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
            } catch (InstantiationException var6) {
            } catch (ClassNotFoundException var7) {
            } catch (ClassCastException var8) {
            }
        } else {
            var3 = new SecurityManager();
        }

        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }

        System.setSecurityManager(var3);
    }
}

这个构造方法中,我目前所了解到的,就是初始化了AppClassLoader和ExtClassLoader,并且,和这篇博客相关的,我们只需要关心this.loader这个全局变量,这个全局变量存放的是AppClassLoader的对象信息

构造一个自定义类加载器

我们要自定义一个类加载器,只需要继承classLoader,并重写findClass()方法即可

class MyClassLoaderTest extends ClassLoader {

    private String loadPath;

    public String getLoadPath() {
        return loadPath;
    }

    public void setLoadPath(String loadPath) {
        this.loadPath = loadPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }

    private byte[] loadByte(String className) throws Exception {
        className = className.replaceAll("\\.", "/");
        FileInputStream fileInputStream = new FileInputStream(loadPath + "/" + className + ".class");
        int available = fileInputStream.available();
        byte[] data = new byte[available];
        fileInputStream.read(data);
        fileInputStream.close();
        return data;
    }
}

那么,我在使用的时候,只需要调用new MyClassLoaderTest()即可

new MyClassLoaderTest()
我们说,自定义的类解析器对应的parentClassLoader,就是在空参构造函数中被赋值的
因为MyClassLoaderTest继承了ClassLoader,所以,会调用到ClassLoader的空参构造函数

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

我们会发现,parentClassLoader就是getSystemClassLoader()返回的,

java.lang.ClassLoader#getSystemClassLoader
	java.lang.ClassLoader#initSystemClassLoader
// 这个方法中的其他变量我们可以暂时先不关心,我们看到有获取到一个Launcher对象
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            // 在这里调用其getClassLoader()方法,将返回的值,赋值给scl,而这个scl就是入参中的parent
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

这里的getClassLoader()返回的就是上面我说明的,需要特别关注的this.classLoader这个全局变量
scl是从l.getClassLoader()这个方法获取到的结果,那我们看下这个方法

sun.misc.Launcher#getClassLoader
public ClassLoader getClassLoader() {
        return this.loader;
}

可以看到,这里的getClassLoader,就是博客上面 Launcher 这个方法中赋值的this.loader;所以,通过上面的代码,可以发现,我们自定义的类解析器,是在初始化的时候,指定了parent为AppClassLoader

自定义类加载器

1、类加载器

类加载器ClassLoader的作用有两个:

①是用于将class文件加载到JVM。
②是用于判断JVM运行时两个类是否相等。

2、类加载的时机

类的加载可分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

  • 遇到new(new
    一个实例对象的时候)、getstatic(获取一个类的静态字段的时候)、putstatic(设置一个类的静态字段的时候)、invokestatic(调用一个类的静态方法的时候)这4条字节码指令时。
  • 对类进行反射调用时。
  • 初始化一个类时,如果父类还没有初始化,则先加载其父类并初始化(但是初始化接口时,不要求先初始化父接口)
  • 虚拟机启动时,需要指定一个包含main函数的主类,优先加载并初始化这个主类。

显式加载

显示加载包含以下几种情况:

  • 通过Class.forName()加载
  • 通过ClassLoader的loaderClass方法加载
  • 通过ClassLoader的findClass方法

3、Class.forName()加载类

Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。

先看看Class.forName()的源码:

/**
  * 参数解释:
  * 1、className:要加载的类名
  * 2、true:class被加载后是否要被初始化。初始化即执行static的代码(静态代码)
  * 3、caller:指定类加载器
  */
public static Class<?> forName(String className) throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以看出来最后正在实现forName()方法的是forName0这一个native方法。而使用forName0需要传递的参数之一,就是ClassLoader类加载器。因此可以知道Class.forName()本质还是使用classloader来进行类的加载的。

4、使用ClassLoader加载类

  • ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
  • loadClass()
    方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的父类classloader里面是否已经加载了目标类,如果没有找到就会让父类Classloader尝试加载,如果父类classloader都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。这实际上就是双亲委派机制的原理。

看一下loadClass()的源码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //查看类是否已被加载,findLoadedClass最后调用native方法findLoadedClass0
            Class<?> c = findLoadedClass(name);
            //还未加载
            if (c == null) { 
                long t0 = System.nanoTime();
                try {
                    //若有父类加载器,则调用其loadClass(),请求父类进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //若父类加载器为null,说明父类为BootstrapClassLoader(该类加载器无法被Java程序直接使用,用null代替即可),请求它来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    //加载失败,抛出ClassNotFoundException异常
                }
                //父类无法加载,调用findClass方法,尝试自己加载这个类
                //注意:在这个findClass方法中,目前只是抛出一个异常,没有任何进行类加载的动作
                //因此,想要自己进行类加载,就要重写findClass()方法。
                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;
        }
    }

ClassLoader 的 findClass() 方法是需要子类来覆盖重写的,不同的加载器将使用不同的逻辑来获取目标类的字节码。得到字节码之后会调用 defineClass() 方法将字节码转换成 Class 对象。

findClass()的源码如下:

protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name); //仅抛出异常
}

  • 可见,ClassLoader的findClass()方法是没有具体实现的,如果要自定义类加载器,就需要重写findClass()方法,并且配合defineClass()
    方法一起使用,defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。
  • 以上就是CLassLoader进行类加载的简单流程。

虽然Class.forName()方法本质上还是使用Classloader来进行类的加载的,但它和使用Classloader来进行类加载依然有着区别:

①Class.forName()方法除了将类的字节码加载到jvm中之外,还会执行类中的static块,即会导致类的初始化。Class.forName(name, initialize, loader)带参方法也可以指定是否进行初始化,执行静态块。

②ClassLoader只是将类的字节码加载到jvm中,不会执行static中的内容,即不会进行类加载,只有在newInstance才会去执行static块。

5、自定义类加载器

我们知道,除了BootstrapClassLoader是由C/C++实现的,其他的类加载器都是ClassLoader的子类。所以如果我们想实现自定义的类加载器,首先要继承ClassLoader。

根据我们前面的分析,ClassLoader进行类加载的核心实现就在loadClass()方法中。再根据loadClass()方法的源码,我们可以知道有两种方式来实现自定义的类加载,分别如下:

①如果不想打破双亲委派机制,那么只需要重写findClass方法。
②如果想要打破双亲委派机制,那么就需要重写整个loadClass方法。

如果没有特殊要求,Java官方推荐重写findClass方法,而不是重写整个loadClass方法。这样既让我们能够按照自己的意愿加载类,也能保证自定义的类加载器符合双亲委派机制。

明确了如何实现,我们只需要两步就可以实现自定义的类加载器:第一步是继承classloader,第二步是重写findClass方法。

不过由于在findClass()内需要调用defineClass()方法将字节数组转换成Class类对象,因此要先对输入的class文件做一些处理,使其变为字节数组。

实现自定义的类加载器:

public class MyClassLoader extends ClassLoader{
    //默认ApplicationClassLoader为父类加载器
    public MyClassLoader(){
        super();
    }

    //加载类的路径
    private String path = "";

    //重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] dataByte = new byte[0];
        try {
            dataByte = ClassDataByByte(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return this.defineClass(name, dataByte, 0, dataByte.length);
    }

    //读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组
    private byte[] ClassDataByByte(String name) throws IOException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
        name = name.replace(".", "/"); // 为了定位class文件的位置,将包名的.替换为/
        is = new FileInputStream(new File(path + name + ".class"));
        int c = 0;
        while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流
            arrayOutputStream.write(c);
        }
        data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组
        is.close();
        arrayOutputStream.close();
        return data;
    }
}

使用自定义的类加载器:

public static void main(String[] args) {
    MyClassLoader myClassLoader = new MyClassLoader();
    Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader");
    clazz.newInstance();
}

Java类加载器(类加载的流程、三大类加载器BootstrapClassLoader、ExtClassLoader、AppClassLoader)

一、三大类加载介绍
1.1、BootstrapClassLoader
BootstrapClassLoader是顶级加载器,默认加载的是%JAVA_HOME%中lib下的jar包和class类文件,他也是ExtClassLoader的父类,但是不是继承(extends)关系,是ExtClassLoder中有一个parent变量是BootstrapClassLoader

1.2、ExtClassLoader
ExtClassLoader扩展类加载器,负责加载%JAVA_HOME%中lib/ext文件下的jar包和class类文件,ExtClassLoader加载器是AppClassLoader的父类,当然也不是继承(extends)关系,也是类中有parent变量

1.3. AppClassLoader
AppClassLoader(应用程序加载器/系统类加载器)是自定义加载器的父类,负责加载classPath下的类文件,平时引用的jar包以及我们自己写的类都是这个加载器进行加载的,同时AppClassLoader还是线程上下文加载器,如果想实现一个自定义加载器的话就继承(extends)ClassLoader来实现.

注: 当然了也可以自定义类加载, 通过继承ClassLoader来实现.

二、类加载的流程
2.1、向上委派
AppClassLoader是加载我们自己编写的class类的,当他遇到一个新的class类的时候,不会直接进行加载,而是向上委派给ExtClassLoader,向上委派就是去查找ExtClassLoader是否缓存了这个class类,如果有则返回,如果没有则继续委派给BootstrapClassLoader,如果BootstrapClassLoader中缓存有则加载返回.

2.2、向下查找
开始进行向下查找了,就意味着当前class类向上委派到BootstrapClassLoader时还是没有该类的缓存,此时BootstrapClassLoader会查找加载自己路径也就是%JAVA_HOME%/lib下的jar与class类文件,如果有则加载返回,没有则继续向下查找。ExtClassLoader也是做同样的操作。查找加载ExtClassLoader对应路径的文件,如果有则加载返回,没有则继续向下到AppClassLoader查找加载,AppClassLoader是加载classPath也就是我们程序员自己编写的class类,如果AppClassLoader找不到则会抛出找不到class类异常

2.3、流程简介
向往委派是到顶层类加载器为止,向下查找是到发起的加载器为止,如果是有自定义类加载的情况,发起和截至会是这个自定义加载器。

2.4、作用
这样做的原因主要是为了安全,避免程序员编写类动态替换Java的核心类比如说String,同时也是避免了相同的class类被不同的ClassLoader重复加载

2.5、​​​​​​​类加载简图
java的类加载器以及如何自定义类加载器_第3张图片

类加载器如何打破双亲委派加载机制(SPI原理)

1.类加载器命名空间可见性
子类加载器可以见到父类加载器加载的类,而父类加载器看不见子类加载器加载的类

2.打破双亲委派加载机制
1.双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类了
2.双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()
双亲委派模型的这个模型存在一些缺陷,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
以JDBC为例:

//传统加载方式 1
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
//传统加载方式 2
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
//SPI加载方式
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

SPI加载机制中,不需要手动设置驱动为com.mysql.jdbc.Driver。DriverManager有一个静态代码块,这会在执行getConnection前运行,即loadInitialDrivers(),此方法会调用ServiceLoader.load(Class service, ClassLoader loader)寻找ClassPath:META-INF/services文件夹下面java.sql.Driver的内容,即实现类。load(
由于ServiceLoader位于rt.jar包下面(并没有继承ClassLoader)是启动类加载器加载的,所以是无法看见厂商的实现类jdbc.Driver,由父类加载器加载的类是看不见子类加载器加载的类的。所以此时采用自己传入的loader结合Thread.setContextLoader(),将jdbc.Driver偷偷加载到内存,并通过以下详细基本原理,进而实现SPI机制

java的类加载器以及如何自定义类加载器_第4张图片
3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。

3.基本原理
当前项目中定义好接口,实现类不在当前类路径下。实现类实现当前项目提供的接口。在当前项目中调用自定义classLoder.load().根据双亲委托机制,会先尝试使用父类加载器加载,加载不成功则使用子类加载器。子类加载器加载当前Student1类,需要用到Student接口,而Student接口是使用父类加载器加载的(在类路径下面),由于父类加载器加载的类对于子类可见,则不会报错).拿到反射实例的class后调用反射(此处不能直接new ,直接new或者直接使用Student1都会造成主动使用,从而造成appClassLoder来加载这个类,由于AppclassLoder无法加载这个类,父类加载器无法访问子类加载器加载的类,此时就会报错)。根据预先定义好的接口Student,就可以使用这个具体实现类的某些方法了
java的类加载器以及如何自定义类加载器_第5张图片

/**

@author: logan
@Date: 2019/10/30 13:15
@Description:
*/
public class Test01 {
public static void main(String[] args)

throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//这个类class的路径
String classPath = "C:\\Users\\Administrator\\Desktop\\test\\com\\student\\";

MyClassLoader myClassLoader = new MyClassLoader(classPath, "my");
String packageNamePath = "Student1";


Class<?> a = myClassLoader.loadClass(packageNamePath);

System.out.println("类加载器是:" + a.getClassLoader());
Student student = (Student) a.newInstance();
student.doSth();
}
}

你可能感兴趣的:(java,开发语言)