Java类加载器

类加载器顾名思义就是加载类的工具,在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文件

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值