Java类加载器

    技术2022-07-11  93

    类加载器顾名思义就是加载类的工具,在Java中用到类的时候,JVM首先要把类的字节码文件加载到内存中来,大家可能在开发的过程中经常会遇到java.lang.ClassNotFoundExcetpion这个异常,出现这种异常的原因就是类加载器找不到要加载的类了

    先看一下段代码

    public class ClassLoaderTest { public static void main(String[] args) { ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); while (classLoader !=null){ System.out.println("classLoaderName:"+classLoader.getClass().getName()); classLoader = classLoader.getParent(); } } }

     

    在这个小例子中,为我们打印出了两个类加载器,其实在Java中JVM预定义了三种类加载器,每个类加载器都有自己所负责的加载类的范围

    三种预定义的类加载器分别是

    Bootstrap:引导类加载器,最顶层的一个类加载器,这个类加载器不是Java类它是C++写的二进制代码,它嵌套在JVM内核里,也就是说JVM启动的时候BootStrap就启动了,负责加载核心Java库,存储在<JAVA_HOME>/jre/lib/rt.jar 中的类,或者加载-Xbootclasspath选项指定的jar包 ClassLoader intClassLoader = int.class.getClassLoader(); System.out.println("intClassLoader="+intClassLoader);//intClassLoader=null

    如上两行代码,随便从rt.jar包中拉出来一个类,我们打印它的类加载器,结果为空,因为他们的类加载器是Bootstrap,它不是JAVA类,所以返回null

    ExtClassLoader :扩展类加载器,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中AppClassLoader:系统类加载器, 它负责将 (java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库 加载到内存中

    说到类加载器,不得不说一下类加载双亲委派机制,如下图

    JVM在加载类时默认采用的是双亲委派机制,当AppClassLoader收到加载类的需求时,AppClassLoader委托给它的上一级ExcClassLoader,ExcClassLoader又委托给Bootstrap去加载,Bootstrap就去它负责的jar包或目录下去查找要加载的.class文件,当他找到的时候就返回,如果它找不到,它就告诉ExcClassLoader说我找不到加载不了,你去加载吧,ExcClassLoader就去它负责的jar包和目录下去查找,它找到就返回,找不到就告诉AppClassLoader说,我也找不到加载不了,你去加载吧,AppClassLoader就去找,这样一级级的往下查找,当AppClassLoader也找不到需要加载的类时,就会抛出ClassNotFoundExcetpion异常。需要说明的是当所需要加载的类指定了某个高级的类加载器加载时,它不会向比它更低级的类加载器去加载,就像AppClassLoader找不到需要加载的类时,不会向比它低级的MyClassLoad1和MyClassLoad2去查找

    JVM在加载第一个类的时候到底使用哪个类加载器呢?

    先它会使用当前线程的类加载器去加载线程中的第一个类,thread.getContextClassLoader();可以获取到线程的类加载器,可以thread.setContextClassLoader(classLoader);设置线程的类加载器。如果类A中引用了类B,JVM将使用类A的ClassLoader来加载类B。也可以指定类加载器去加载一个类ClassLoader.loadClass("com.lp.beans.PersonBean");

    自定义类加载器

    AppClassLoader的类继承关系图

    ExtClassLoader的类继承关系图

    我们看到都会继承ClassLoader这个抽象类,自定义类加载器也需要继承这个抽象类。自定义类加载器,需要用到三个ClassLoader中的三个方法,loadClass,findClass,defineClass。下面我们看看他们都做了什么

    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) {//没有被加载过,执行下面的逻辑 // ----------------------------------------------------------------- /** * 中间这一块的逻辑就是,把需要加载的类,使用委托机制交给所有的上级类加载器去加载 * 如果加载到了,那就返回class,如果找不到,就使用findClass 方法自己查找 * 我们如果自己定义类加载器只需要覆盖findClass方法定义如何查找就可以了 * * 如果我们重定义了loadClass,那么就不会从上级,上上级。。。loader 中加载类了, * 除非我们自己定义从上级查找逻辑, */ long t0 = System.nanoTime(); try { //如果它的上一级加载器存在,则使用它的上一次加载器 加载需要加载的类 if (parent != null) { //这里递归调用 c = parent.loadClass(name, false); } else {//如果上一级加载器不存在,则使用Bootstrap加载需要加载的类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 所有上级都找不到需要加载的类 抛出异常 这里什么都没做,是还需要执行下面的逻辑 } // ----------------------------------------------------------------- if (c == null) { // If still not found, then invoke findClass in order // to find the class. //如果它的上级,上上级。。。loader 都找不到需要加载的类,那么就使用自己的loader来加载类 long t1 = System.nanoTime(); //这里使用findClass来加载类,自己定义查找逻辑 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,其实里面的逻辑很简单,自己要从哪个地方什么路径下去加载.class文件,之后怎么加载到JVM呢,就是通过defineClass方法,我们看到它是一个final方法,所有我们直接调用它就可以了,具体里面有加载前预处理,和加载逻辑预计加载后的东西大家去看吧

    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; }

    了解上面的内容后,我们开始定义自己的类加载器

    public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String fullName) throws ClassNotFoundException { try { //将类名转换成路径 com.lp.loader.Person 转换成 com\lp\loader\Person.class fullName = fullName.replace(".", File.separator)+".class"; //根据路径读取.class 文件 FileInputStream in = new FileInputStream(classPath + fullName); //转换成字节 byte[] b = new byte[in.available()]; in.read(b); //调用父类提供的 defineClass 获取class return defineClass(null, b,0,b.length); } catch ( Exception e) { e.printStackTrace(); } return null; } }

    然后我们再定义一个Person类,如下

    public class Person { @Override public String toString() { return "Person{}"; } }

    找到它再类路径下的Person.class ,复制一份到E:\\class,把这个Person类删除,原来类路径下的Person.class也删除

    public class ClassLoaderTest { public static void main(String[] args) throws Exception { //获取classpath E:/test/out/production/test/ 这是我们当前项目的类路径 // String classPath = Thread.currentThread().getContextClassLoader().getResource("").getFile(); //定义类路径 我们把需要加载的文件放到这下面 String classPath = "E:\\class\\"; //创建自定义类加载器对象 MyClassLoader myClassLoader = new MyClassLoader(classPath); //加载 E:\class\ 路径下的类 Class<?> clazz = myClassLoader.loadClass("Person"); //加载除了的类创建对象 Object obj = clazz.newInstance(); //获取toString方法 Method toString = clazz.getMethod("toString"); //执行方法 打印 System.out.println(toString.invoke(obj)); } }

    打印结果 

    这样我们的类加载器就完成了,可以加载任何路径下的.class文件

     

    Processed: 0.010, SQL: 9