JVM虚拟机系列 - classloader机制

什么是类加载器?

通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性

类相等机制?

相等的规则:

这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。

注意,是类相等机制。不是对象相等。如果一个类文件class文件A,由同一个虚拟机,两个不同的类加载器加载,那么这就是两个不同的类。

实例:

public class TestClassLoader {

    public static void main(String[] args) {
        ClassLoader classLoader1 = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf("." )+1)+".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] bytes = new byte[inputStream.available()];
                    inputStream.read(bytes);
                    return defineClass(name,bytes,0,bytes.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return super.loadClass(name);
            }
        };
        try {
            Class<?> aClass = classLoader1.loadClass("com.chengjianyu.classloader.demo.TestClassLoader");
            Object o = aClass.newInstance();
            System.out.println(o.getClass());
            System.out.println(o instanceof TestClassLoader);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

输出的结果:

class com.chengjianyu.classloader.demo.TestClassLoader
false

原因:

这是因为Java虚拟机中同时存在了两个TestClassLoader类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是由我们自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类,做对象所属类型检查时的结果自然为false。

三层类加载器

启动类加载器(Bootstrap Class Loader)

这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。

扩展类加载器(Extension Class Loader)

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

应用程序类加载器(Application Class Loader)

这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码

双亲委派模型的工作过程:

1)某一个类加载器收到加载类的请求,首先不会自己加载,而是把请求委派给父类进行加载。

2)父类加载器收到加载请求后,会继续委派。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。

3)启动类加载器收到加载请求,尝试加载,如果完成加载则返回。如果无法完成加载,则由子类加载器尝试完成加载。

好处:

一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。

假如说,我自己搞了一个Java.lang.Object,按照双亲委派原则,我的最终会被bootstrapClassLoader加载,但是rt.jar里面已经有这个类了,肯定会率先加载rt.jar下的,所以我的永远无法被加载到。

ClassLoader加载代码片段:

破坏双亲委派模型

第一个破坏的场景:

一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么?(因为jndi是在rt.jar中,但是这些spi的实现在各自的包,不再rt.jar中,如何loadClass呢?)

第二个破坏的场景:热加载机制,这个就有点复杂了。

解决办法:

线程上下文类加载器(Thread Context ClassLoader)

这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

例子:

dubbo内这里就是采用设置Thread的classLoader去加载所有invoker的spi的实现

Tomcat类加载实现

网上已经有一个非常详细的文章,参考:https://www.cnblogs.com/aspirant/p/8991830.html

摘抄一段过来:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见

Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。具体的加载逻辑位于WebAppClassLoaderBase.loadClass()方法中,代码篇幅长,这里以文字描述加载一个类过程:

  1. 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
  2. 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  3. 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
  4. 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。

第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一样是违反了双亲委托。

总结

1)什么是类加载器,同一个class文件,不同类加载器进行加载,得到的也是不同的class对象。

2)三层类加载器(BootsrtapClassLoader, ExtClassLoader, AppClassLoader),双亲委派机制机制,首先自己不加载,交给父类去加载,父类加载不到再由子类加载。

3)spi机制,需要破坏双亲委派,通过线程上下文加载器进行解决。

4)tomcat的类加载机制,从一定程度上是破坏了双亲委派机制,因为webAppClassloader,会率先自己加载而不是交由父类。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值