Java ClassLoader

本文深入介绍了Java中的ClassLoader机制,包括其工作原理、默认的类加载器类型及其关系、双亲委派模型,以及如何自定义ClassLoader等内容。通过理解ClassLoader,可以更好地解决类加载异常等问题。

Java ClassLoader

Java ClassLoader 是JVM对类加载的基础,通过研究ClassLoader,我们可以解惑 Tomcat 的多项目加载等机制。

什么是ClassLoader

ClassLoader 是JVM加载class对象的一个类,通过这个类,可以把class载入到JVM虚拟机中,形成可执行代码。

默认ClassLoader

一旦启动JVM虚拟机,就会默认加载几个ClassLoader,作为基础加载类型。其基本关系为:

ClassLoader基本关系

Bootstrap ClassLoader

该对象由C/C++实现,用于加载jre/lib下的所有JAR,特别是rt.jar。 之后,会初始化一个Launcher.java(sun.misc.Launcher)的对象,用于配置 ExtClassLoader和 AppClassLoader。

ExtClassLoader

该对象主要是用于加载/jre/lib/ext/*.jar 的数据

AppClassLoader

用于加载 系统环境变量中的classpath指向的jar 以及 -cp | -jar 指定的JAR包

线程上下文ClassLoader

在Launcher中,同时还会设置AppClassLoader作为线程上下文的ClassLoader。

如果不使用Thread#setContextClassLoader修改对应线程的ClassLoader,那么会继承父线程的ClassLoader对象

通过 线程上下文ClassLoader,可以非常方便的加载SPI内容,比如说JDBC。

双亲委派模型

Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

  1. 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.

  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

所以,默认的加载class优先级为:BootstrapClassLoader-> ExtClassLoader ->AppClassLoader 。

ClassLoader 空间命名机制

空间命名是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。公式为:

FULL_CLASS_IN_JVM_ID = CLASS_LOADER_ID + CLASS_NAME

也就是说即使两个类的全限定名是相同的,但是因为不同的 ClassLoader加载了此类,那么在JVM中它是不同的类。

自定义ClassLoader

我们可以通过编写ClassLoader,加载一些特殊位置的代码,比如说从网络上获取。下面来介绍一下如何自定义ClassLoader,查看ClassLoader,可以发现几个重要的API。

loadClass

这个方法,包含了读取缓存,双亲委托的功能,如果想打破双亲委托模型,则需要重写该方法。

触发loadClass的条件:

  1. 手动调用loadClass方法。
  2. 处理类A中的依赖类B的时候for new B()
    1. JVM查询加载类A的ClassLoader的clz缓存,判断clz缓存中是否存在类B。
    2. clz缓存中存在类B,则直接返回类B。
    3. clz缓存中不存在类B,则调用loadClass进行加载类B。

注意:loadClass 会优先查询clz缓存。

findClass

这个方式,就是通过className获取具体class的方法了。可以看到ClassLoader中

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

就直接抛出异常,如果想自定义ClassLoader,且不想打破双亲委托模型,那么只需要简单的重写该方法即可。

defineClass

通过这个方法,就可以把二进制文件转换为JVM内部的Class对象,且这个方法不能被重写,也应该是唯一的入口。

值得注意的是,即使你重写了ClassLoader,如果加载java.的类,JVM会拒绝你,因为只能通过bootstrap加载java. 的Class

常见的ClassLoader API

// 通过当前类的类加载器加载(会执行初始化)
Class.forName("二进制名称");
Class.forName("二进制名称", true, this.getClass().getClassLoader());

// 通过当前类的类加载器加载(不会执行初始化)
Class.forName("二进制名称", false, this.getClass().getClassLoader());
this.getClass().loadClass("二进制名称");

// 通过系统类加载器加载(不会执行初始化)
ClassLoader.getSystemClassLoader().loadClass("二进制名称");

// 通过线程上下文类加载器加载(不会执行初始化)
Thread.currentThread().getContextClassLoader().loadClass("二进制名称");

new 关键字

为何会说道new关键字,因为new关键字,也是利用ClassLoader进行加载具体的类的!

new使用的ClassLoader

经过深入的研究发现,**发现new的时候,采用的ClassLoader是关联到加载当前类的ClassLoader。**比如说:

public class Test{
    public Test(){
        print();
    }
    public void print(){
        Runnable a = new Runnable(){
            void run(){
                System.out.println("Hello World");
            }
        }
    }
}

现在我们通过自定义的SelfClassLoader加载Test类,并且进行newInstance()。那么,在上述代码中,new Runnable(){} 这个匿名类,也会通过自定义的SelfClassLoader加载

跨ClassLoader引用

我们定义了一个接口(People),被ClassLoader1加载,然后我们定义了一个具体的实现类,被ClassLoader2加载。

那么,我们是否可以使用 CL1#People p = (CL1#People)CL2#Man 来进行强制转换呢?根据ClassLoader的双亲委派模式,其实在实例化Man的时候,因为Man的接口来自于CL1#People,所以,这种转换是可以的,只要ClassLoader2 的parent 是 ClassLoader1

解决了这个问题,也就顺便的解决了TOMCAT 接口都是由COMMON ClassLoader进行加载Servlet API,而具体的Servlet实现由WebClassLoader 进行加载,并且它们的转换是没有问题的。

AOP

Java

这里,再说一下AOP的知识,AOP的一般实现有:

  1. 编译时 : 通过直接重写class 类实现,AOP功能,比较复杂

  2. 运行时-载入Class阶段 : 使用特殊的ClassLoader,比较复杂

  3. 运行时-动态修改Class : 读取某一个类的byte[]内容,然后使用ClassLoader#defineClass方法,申请一个类,然后使用它。

  4. 运行时-JVM API:通过Java提供的API,进行生成代理。

可以发现,前三种都是通过修改class的byte来达成AOP的目的。而最后一种,是通过JVM提供的API来达成AOP的。

C语言

因为C语言的特性:基于偏移量寻址,而非符号,导致C语言基本上无法实现在运行时进行AOP操作,所以常见的方式:在编译源代码的时候,进行AOP处理。但是这种方式,还是有许多不足之处。

总结

通过了解ClassLoader,可以学习到JVM的类加载机制,在具体的工作中,可以利用这些特性,比较方便的解决一些 ClassNotFoundException, CAST 异常等问题。

参考

深入探讨 Java 类加载器

Java魔法堂:类加载器入了个门

Java Classloader机制解析

转载于:https://my.oschina.net/darkgem/blog/620250

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值