JVM-3 类加载机制(下)

这篇博客承接上文 JVM-3 类加载机制(上),内容包括:
(1)自定义类加载器,(2)类加载器的命名空间,(3)双亲委派机制的缺点以及线程上线文类加载器,(4)实际使用类加载器的例子。

1.自定义类加载器

一个运行的程序会不断地将类加载进jvm内存,可以使用jdk提供的三种类加载器来完成需求。有时候对类加载有了额外的要求,如扩展类的加载途径,或者通过类加载器对类进行隔离,这个时候需要自定义类加载器就派上了用场。
下面介绍一下自定义类加载器的实现过程:

(1)新增一个类,继承ClassLoader类,并重写findClass方法;
(2)将class文件读取到二进制数组中;
(3)调用defineClass方法并传递(3)中得到的二进制数组,返回class对象;
(4)客户端新建一个类加载器对象,调用loadClass方法并传入待加载的类的全限定名;

具体代码如下:
自定义类加载器部分代码:

package com.shengyu.classLoader;

import java.io.*;

public class MyClassLoader extends ClassLoader{
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) {
        byte[] classData = getData(name);
        return defineClass(name, classData, 0, classData.length);
    }
    private byte[] getData(String name) {
        File file = new File(buildClassName(name));
        if (file.exists()) {
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        } else {
            return null;
        }
    }

    private String buildClassName(String name) {
        // FixME:空指针判断;此代码用于类加载距离,涉及空指针findbugs暂不处理
        String[] name_arr = name.split("\\.");
        return path + name_arr[name_arr.length-1] +".class";
    }
}

客户端代码

package com.shengyu;

import com.shengyu.classLoader.MyClassLoader;

public class Application {
    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader loader = new MyClassLoader("D:\\Data\\Code\\test\\");
        Class clazz = loader.loadClass("com.shengyu.vo.Car");
        System.out.println(clazz.getClassLoader());
    }
}

简要讲解:
上面实现的类加载器可以去任何文件系统路径下加载class文件,只需要在生成类加载器的时候指定路径即可(构造函数入参)。
扩展:
自定义类加载器的逻辑中调用了defineClass方法, 该方法内部调用native方法为我们实现了缓存。因此自定义类加载器和jdk提供的类加载一样,都有缓存机制。在调用loadClass时,内部会调用一个findLoadedClass方法查询是否该类已被缓存。已被加载过的类都会被缓存,当程序需要再次加载某个类时,类加载器会先从缓存中获取,没有才会去加载;
这是为什么修改了class文件后,必须重启jvm进程才会生效。

样例
case1:因为Car.class此时位于classpath中,Car这个类被系统类加载器加载。
在这里插入图片描述
case2:在类路径下删除Car.class文件后, Car这个类被自定义类加载器加载。
在这里插入图片描述
=> 这是双亲委派模型带来的结果:
case1:

自定义类加载器MyClassLoader 根据类的全限定名"com.shengyu.vo.Car"去加载时,首先会委派给父类加载器,即系统类加载器(继续往上->扩展类加载器->引导类加载器,这两个都不能加载,最后使用系统类加载器加载)。由于Car这个类位于类路径下,系统类加载器可以轻松加载这个类。即Car的类加载器是系统类加载器。
case2:
系统类加载器在classpath中根据"com.shengyu.vo.Car"找不到资源,无法加载,返回给MyClassLoader,由MyClassLoader完成加载;即Car的类加载器是MyClassLoader。

系统类加载器加载进来的类和被MyClassLoader加载进来的类有什么区别呢?答案在下一节"类的命名空间中介绍"。

2.类加载器的命名空间

命名空间:每个类加载器都有自己的命名空间,命名空间由该类加载器以及器父类加载器所加载的类组成。每一个命名空间中不会出现类的全限定名完全相同的两个类(jvm中的类的唯一性是由 类的全限定名和加载该类的类加载器组成),不同的命名空间中可以出现。

因此,对于jvm的类加载器有以下规则:

1.同一个命名空间中不允许有两个相同的类,不同的命名空间可以。
2.不同类加载器加载的同一个类,相互不可见。
3.子类对父类加载器加载的类可见;
4.父类对子类加载器加载的类不可见;

3.线程上下文类加载器

java提供了很多SPI(SPI的概念可参考JAVASE-16 SPI),SPI接口定义在java的核心类中,由引导类加载器加载;SPI实现类在类路径下,由系统类加载器加载。
SPI接口中的代码需要加载由具体的实现类。按照双亲委派模型,引导类加载器找不到SPI的实现类。这不是个设计缺陷吗?

上面的表述可以抽象出一个简单的例子:
在这里插入图片描述
如上图所示:类A由扩展类加载器(标记为ExtLoader), 类B由系统类加载器加载;类A中有一个test方法,方法接收Object类型的参数;当A类对象调用这个test函数且传入B对象时,就会报错。
原因分析:
因为B由系统类加载器加载,加载A类的扩展类加载器找不到类B。细节一点就是:A的方法调用的时候,调用findClass查询这个B类是否已加载,发现没有被扩展类加载器加载,所有尝试去加载,因为找不到而报错。引入线程类上下文这个类加载器,是给双亲委派机制打开了一个逃生门。我个人认为这是无奈之举,为了弥补双亲委派机制的设计缺陷。
在jdk1.2中同时也引入了双亲委派机制和线程上下文类加载器。
从java.lang.Thread中的getContextClassLoader()来获取线程上下文类加载器;如果没有使用setContextClassLoader(classLoader),线程将继承其父线程的上下文类加载器,如果父类也没设置,默认返回的就是系统类加载器。
线程上下文是如何解决本节开头指出的问题呢,以一个例子来说明。
Driver.get
调用DriverManager的静态方法时getConnection方法时,会触发DriverManager的初始化,即会立刻执行static代码块:
在这里插入图片描述
对loadInitialDrivers部分,可以抽出来主要逻辑(去掉了AccessController.doPrivileged部分和异常处理逻辑):

private static void loadInitialDrivers() {
// 第一部分使用线程类加载器加载
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while(driversIterator.hasNext()) {
          driversIterator.next();
    }
// 第二部分显示使用系统类加载器加载
    String drivers = System.getProperty("jdbc.drivers");
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    for (String aDriver : driversList) {
        Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
     } 
}

第一行ServiceLoader.load(Driver.class)的内部逻辑如下:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

即:使用了线程上下文类加载来加载这个类。

4.类加载的使用样例

4.1.Tomcat中对类加载的使用

Tomcat中的类加载架构图如下所示:
在这里插入图片描述Common、Catalina、Shared 和 WebApp 四个类加载是 Tomcat 自己定义的,它们分别用于加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。
其中 WebApp 类加载器:每一个web程序对应一个WebApp 类加载器实例;
JSp类加载器:每一个 JSP 文件对应一个 Jsp 类加载器。
这样可以实现类路径的共享和隔离:

1.对于tomcat和web共享资源,使用Common加载
2.tomcat私有,使用Catalina加载
3.所有web共享,但对tomcat隔离的使用shared
4.每个webApp私有的使用WebApp加载
5.每个Jsp文件使用一个JSP类加载器去加载

4.2.Spring中对类加载的使用

Spring项目使用tomcat部署时,按照上面的类加载器架构。对于共享的资源,spring使用common或者shared加载;web业务代码使用webApp类加载器加载;
Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类;
此时形成了一个熟悉的格局:
父类记载器加载的类调用了子类加载器加载的类。
这样情况,需要借助线程上下文类加载器来规避,此时,可以使用Thread.setContextClassLoader方法来设置WebApp作为线程上下文加载器,问题完美解决。

4.3.JSP热部署涉及

JSP现在看起来有点过时了,但是用来介绍虚拟机的加载机制,是个很好的例子。
JSP在tomcat是支持热部署的:程序运行时,可以随时修改jsp文件,替换到容器后,不需要重启服务,“很快”就会生效。
很快的意思时有个秒级别的延误(tomcat7默认是4s).
每经过4s,tomcat就会对比jsp文件和class文件的时间戳,判断jsp文件是否过时了。如果过时了,就需要重新编译。
在jvm中一个类是不能被一个类加载器加载两次的,但是可以使用不同的类加载器对这个类进行加载。说到底,类加载器也是对象,可以被GC和创建。
JSP的热部署实现原理是:每次jsp文件更新时,就使用一个新的类加载器加载jsp生产的servlet.class文件。

4.4.OSGI

类加载器的另一个很好的应用时OSGI,这个部分可以参考《深入理解JAVA虚拟机》第9章类加载及执行子系统的案例分析。
有这么一句话“学习类加载器的知识,就推荐去看OSGI的源码”。
所以,如果想学号类加载器,还是推荐去研究一下OSGI的类加载器架构设计。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值