首页 > 编程语言 >【Java虚拟机探究】10.类装载器(下)

【Java虚拟机探究】10.类装载器(下)

时间:2023-04-03 16:01:49浏览次数:61  
标签:10 Java name 虚拟机 ClassLoader public findClass class 加载


上一篇我们总结了类加载器的基本原理和与应用程序相关的ClassLoader,并提到了双亲委派模式。本篇继续探讨类加载器的双亲委派模式,以及如何破坏双亲委派模式达到加载底层类的目的。

1.双亲委派模式的问题

我们回顾一下原来的应用程序的ClassLoader的加载模式:

【Java虚拟机探究】10.类装载器(下)_类加载器


除了顶层的ClassLoader,每一个ClassLoader都有一个父级ClassLoader,当加载一个类的时候,是自底向上查找,自顶向下查找,所以会出现一个问题,就是顶层的ClassLoader无法加载底层ClassLoader的类,即在启动ClassLoader中,无法启动和加载AppClassLoader里的任何一个类,也没有办法生成这个类的实例对象。

其实双亲委派的做法本意是:

1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系

2)防止自定义类与系统核心类重名,导致程序混乱

但是这种顶层的类加载器不能加载底层的类的方式,会引起一些特殊场景下的加载问题。

2.双亲委派模式的问题场景
最典型的一个例子就是就是,JDK自带的rt.jar是顶层加载器(BootStrap ClassLoader启动加载器)可加载的,但是在该jar包中的javax.xml.parsers包中定义了xml解析的类接口SPI(Service Provider Interface),但是我们去实现SPI的实现类的时候,是我们自己自定义的类,这些类都在应用类加载器(App ClassLoader)中。
此时我们就可以看到两个问题:
1)SPI接口本身,以及产生这些类的工厂类本身,都是在rt.jar中的,是被顶层加载器(BootStrap ClassLoader启动加载器)可加载的。
2)SPI接口的实现类,属于用户开发的应用层类,它们是由应用类加载器(App ClassLoader)定义的实例类。
此时就要求启动加载器(BootStrap ClassLoader)要能访问应用类加载器(App ClassLoader)中的内容。
但是根据上面的双亲委派模式,是无法从顶层加载器去加载底层加载器中的类对象的,但是在这种场景下,确实需要从顶层加载器去加载SPI接口的实现类,此时衍生了以下解决方案:

Thread.setContextClassLoader(ClassLoader cl);
Thread.getContextClassLoader();

这里的意思就是,为一个线程设置一个“上下文”的类加载器(Context上下文),以及获取上下文类加载器。该方法的提出就是为了解决让顶层类加载器去加载底层类加载器的内容。它的基本思想就是,在顶层类加载器中,传入底层类加载器的实例。说白了,该“上下文类加载器”其实不是一个真实的类加载器,而是一个角色,它的作用就是设置一个底层的类加载器,作为顶层的一个“上下文类加载器”。

下面的代码就是rt.jar包中javax.xml.parsers.FactoryFinder的源代码,展示了如何在启动类加载器加载AppLoader的类:

static private Class getProviderClass(String className, ClassLoader cl,
        boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
    try {
        if (cl == null) {
            if (useBSClsLoader) {
                return Class.forName(className, true, FactoryFinder.class.getClassLoader());
            } else {
                cl = ss.getContextClassLoader();
                if (cl == null) {
                    throw new ClassNotFoundException();
                }
                else {
                    return cl.loadClass(className); //使用上下文ClassLoader
                }
            }
        }
        else {
            return cl.loadClass(className);
        }
    }
    catch (ClassNotFoundException e1) {
        if (doFallback) {
            // Use current class loader - should always be bootstrap CL
            return Class.forName(className, true, FactoryFinder.class.getClassLoader());
        }
…..

javax.xml.parsers.FactoryFinder的功能就是,帮启动加载器(BootStrap ClassLoader)加载一个应用类加载器(App ClassLoader)才能加载的类。其中“return cl.loadClass(className)”就是使用上下文ClassLoader的核心,这里的cl就是上下文的类加载器,此时将应用类加载器(App ClassLoader)赋值到上下文加载器中即可。
所以使用线程上下文类加载器,就可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。

3.双亲委派模式的破坏
双亲委派模式是JVM的默认模式,但是不是必须这么做,因为JVM并不强迫我们这么做。例如:
(1)Tomcat
Tomcat有自己的类加载模式,其中的WebappClassLoader类加载器,它会先加载自己的class,找不到再委托parent。
(2)OSGI
OSGI组件开发模式的特点就是可以热加载,模块化比较强。在热加载过程中,各个组件的类可能会被随时停止,或者随时启动加入,它就要求类加载是按照需求来的。所以OSGI对类加载模式进行了很大、很复杂的改造,有一套查找类的算法和机制。它的Classloader形成了一个网状结构,会根据需要自由加载Class,也是完全不符合双亲委派模式的。

4.破坏双亲委派模式实例
下面的实例就是从底层ClassLoader加载类:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class re=findClass(name);
    if(re==null){
        System.out.println("无法载入类:"+name+"需要请求父加载器");
        return super.loadClass(name,resolve);
    }
    return re;
}

上面的方法中可以看到,在加载类的时候,首先是在自己的ClassLoader中进行查找并加载,如果找不到并加载不出来,才回去父级类加载器中寻找并加载。
其中的findClass方法:

protected Class<?> findClass(String className) throws ClassNotFoundException {
    Class clazz = this.findLoadedClass(className);
    if (null == clazz) {
        try {
            String classFile = getClassFile(className);
            FileInputStream fis = new FileInputStream(classFile);
            FileChannel fileC = fis.getChannel();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            WritableByteChannel outC = Channels.newChannel(baos);
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            //省略部分代码...
            fis.close();
            byte[] bytes = baos.toByteArray();

            clazz = defineClass(className, bytes, 0, bytes.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return clazz;
}

在该方法中,对类进行了查找、定义以及加载。首先判断该类是否已经加载过了,如果该类没有被加载,则尝试读取类文件,并使用输入流将该类的二进制byte数据读出来,并调用defineClass将该类真真实实的定义起来。此时就达到了从底层加载类的实例的效果。

 

5.自定义类加载器
自定义一个类加载器,我们需要继承ClassLoader,并且重写父类的findClass()方法即可。
样例代码:

package cn.com.classloader.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader{
    
    public static final String drive = "F:/";
    public static final String fileType = ".class";
    
    public static void main(String[] args) throws Exception {
        MyClassLoader loader = new MyClassLoader();
        Class<?> objClass = loader.loadClass("HelloWorld", true);
        Object obj = objClass.newInstance();
        System.out.println(objClass.getName());
        System.out.println(objClass.getClassLoader());
        System.out.println(obj.getClass().toString());
    }

    //重写父类加载器的findClass方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //调用自定义的loadClassData方法,从drive找到相关的类文件并加载
        byte[] data = loadClassData(name);
        //调用父类defineClass方法,从byte中定义类的实例
        return super.defineClass(name,data,0,data.length);
    }
    
    //根据类名从指定的区域加载类的byte流
    public byte[] loadClassData(String name){
        FileInputStream fis = null;
        byte[] data = null;
        
        try {
            fis = new FileInputStream(new File(drive+name+fileType));//获取文件输入流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int index = 0;
            while((index = fis.read()) != -1){
                baos.write(index);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return data;
    }
}

这里我们重写了父类加载器的findClass方法,从我们制定的磁盘目录加载相关的类文件的byte数据,给父类的defineClass方法生成类的实例。
我们在F盘下放置一个HelloWorld.java的类,其中的代码如下:

public class HelloWorld{
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

然后执行javac进行编译,得到class文件:

【Java虚拟机探究】10.类装载器(下)_加载_02


运行代码,得到结果:

【Java虚拟机探究】10.类装载器(下)_类加载器_03

我们来思考一下,我们重写的findClass方法被父类ClassLoader用在什么地方呢?我们打开ClassLoader的loadClass()方法就知道了:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name))
    {
        // 第一步先检查这个类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null)
        {
            long t0 = System.nanoTime();
            try
            {
                //parent为父加载器
                if (parent != null)
                {
                    //将搜索类或资源的任务委托给其父类加载器
                    c = parent.loadClass(name, false);
                } else
                {
                    //检查该class是否被BootstrapClassLoader加载
                    c = findBootstrapClassOrNull(name);
                }
            } 
            catch (ClassNotFoundException e)
            {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null)
            {
                //如果上述两步均没有找到加载的class,则调用findClass()方法
                long t1 = System.nanoTime();
                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;
    }
}

在上述方法中,首先会先检查该类是否已被加载,如果没有被加载,则委托其父类进行加载,如果没有父类则检查最顶层的Bootstrap ClassLoader是否加载了该类。倘若前面都没有,则该类加载器执行自己的findClass方法,查找和加载该类。而我们自定义类加载器的核心就是这个查找和加载该类,是去我们自己定义的区域内加载。

我们由此可以推断出,所有的类加载器的findClass方法都是根据这个类加载器的特性,去不同的地方来加载类的。例如App ClassLoader就去应用层的classpath中加载类,而顶层的BootStrap ClassLoader就去JDK的classpath中区加载类。

6.使用自定义类加载器实现热替换
在PHP中就有热替换的特性,例如我们在PHP服务器运行的过程中,替换掉一个PHP文件,以后的运行就以替换后的PHP文件进行执行。而除了OSGI,传统的JAVA程序在容器中运行时(例如Tomcat),如果替换了class文件,此时是不会立刻生效,而需要重启容器才可以被重新加载。

下面我们使用破坏双亲委派模式的类加载方式,来实现一个热启动的效果。
首先编写一个运行中要被替换的类Worker:

public class Worker {
    public void sayHello(){
        System.out.println("version:A");
    }
}

这里输出一个"version:A"的语句,来证明该类的版本(我们可以替换为打印"version:B"的一个新类)。
因为JVM提供的类加载器,只能加载指定目录的jar和class,如果我们想加载其他位置的类或jar时。需要编写我们自己的ClassLoader来加载。实现一个ClassLoader,并指定这个ClassLoader的加载路径。有两种方式: 
方式一:继承ClassLoader,重写父类的findClass()方法。
方式二:继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类。

这里使用方式二来编写自定义的ClassLoader,将指定的目录转换为URL路径,然后重写findClass方法:

package cn.com.classloader.test;

import java.net.URL;
import java.net.URLClassLoader;

public class HotClassLoader extends URLClassLoader {

    public HotClassLoader(URL[] urls) {
        super(urls);
    }

    // 打破双亲模式,保证自己的类会被自己的classloader加载
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                c=findClass(name);
            } catch (Exception e) {
            }
        }
        if(c==null){
            c=super.loadClass(name, resolve);
        }
        return c;
    }

}

这里我们继承了URLClassLoader类,URLClassLoader类介绍:
URLClassLoader是在java.net包下的一个类。URLClassLoader继承了ClassLoader,可以理解为它将ClassLoader扩展了一下。一般动态加载类都是直接用Class.forName()这个方法,但这个方法只能创建程序中已经引用的类,并且只能用包名的方法进行索引,比如Java.lang.String,不能对一个.class文件或者一个不在程序引用里的.jar包中的类进行创建。
URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:
* 文件: (从文件系统目录加载)
* jar包: (从Jar包进行加载)
* Http: (从远程的Http服务进行加载)
最后写一个执行类,来进行热启动的模拟:

package cn.com.classloader.test;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class HotClassLoaderMain {
    
    private URLClassLoader classLoader;
    private Object worker;
    private long lastTime;
    private String classDir="G:\\Space-JavaDevelopMent\\JVM-Test\\src\\";
    
    public static void main(String[] args) throws Exception {
        HotClassLoaderMain helloMain=new HotClassLoaderMain();
        helloMain.execute();
    }
    
    private void execute() throws Exception {
        while(true){
            //监测是否需要加载
            if(checkIsNeedLoad()){
                System.out.println("检测到新版本,准备重新加载");
                reload();
                System.out.println("重新加载完成");
            }
            //一秒
            invokeMethod();
            Thread.sleep(1000);
        }
    }
    
    private void invokeMethod() throws Exception {
        //通过反射方式调用
        //使用反射的主要原因是:不想Work被appclassloader加载,
        //如果被appclassloader加载的话,再通过自定义加载器加载会有点问题
        Method method=worker.getClass().getDeclaredMethod("sayHello", null);
        method.invoke(worker, null);
    }

    private void reload() throws Exception {
        classLoader = new HotClassLoader(new URL[] { new URL(
                "file:"+classDir)});
        worker =  classLoader.loadClass("cn.com.classloader.test.Worker")
                .newInstance();
        
    }

    //检查文件是否被更新
    private boolean checkIsNeedLoad() {
        File file=new File(classDir+"cn\\com\\classloader\\test\\Worker.class");
        long newTime=file.lastModified();
        if(lastTime<newTime){
            lastTime=newTime;
            return true;
        }
        return false;
    }
}

我们在的eclipse中创建的java工程的src中的代码被编译后,会放在工作空间该工程下的bin文件夹下:

【Java虚拟机探究】10.类装载器(下)_类加载器_04


所以这里的类加载路径就是工程所在的bin文件夹下。这里我们使用while死循环来模拟一个java容器中间件,反复执行一个类的sayHello方法,然后检测这个类的class是否被改变,如果改变,重新使用自定义类加载器加载该类。

先将程序跑起来:

【Java虚拟机探究】10.类装载器(下)_加载_05


我们这里不去停止服务,而是将Worker类的输出语句改为“version B”:

package cn.com.classloader.test;

public class Worker {
    public void sayHello(){
        System.out.println("version:B");
    }
}

此时我们注意控制台的输出语句,Worker类被重新加载了:

【Java虚拟机探究】10.类装载器(下)_类加载器_06


此时我们的热启动功能完成啦!!!

这里要注意的是,我们获得类以及类的方法实例,是使用的反射,原因是Worker类是定义在我的eclipse的JVMTest工程的classpath下的,我们自定义的类加载器没有指定父加载器,在JVM规范中不指定父类加载器的情况下,默认采用系统类加载器即AppClassLoader作为其父加载器,所以在使用该自定义类加载器时,需要加载的类不能在类路径中,否则的话根据双亲委派模型的原则,待加载的类会由系统类加载器(这里就是AppClassLoader)加载。所以使用反射去调用类路径中类的方法,就不会被默认的系统类加载器先加载。

该系列所有的源代码在我的github下可下载:https://github.com/ZhuYaoGuang/JVM-Test

 

标签:10,Java,name,虚拟机,ClassLoader,public,findClass,class,加载
From: https://blog.51cto.com/u_16012040/6166627

相关文章

  • 【Java虚拟机探究】9.类装载器(上)
    在JVM类要通过类装载器(ClassLoader)进行装载后,才能进行执行。本篇总结了类装载器的一些知识。一、class装载验证流程在第一篇总结中介绍了JVM的内存结构:可以看到class文件首先要通过“类加载器子系统”,才能被加载到内存中处理。那么class文件是怎么通过类加载器加载至内存中的呢......
  • 【FastDFS分布式文件系统】6.FastDFS客户端启动与Java连接
    上一篇我们讲解了如何配置和启动FastDFS客户端,以及客户端上传下载的一些常用命令。那么,在许多需要进行分布式文件上传与下载的系统中,就不能像执行Linux命令一样去上传和下载文件,它们需要使用开发系统的语言去操作客户端使用其命令与服务端进行交互,此时FastDFS......
  • 【送猫超卡、阿里云代金券】动手体验 SAE+云效 10 分钟快速打通 CI/CD 流水线
    作者:ServerlessServerless应用引擎SAE是阿里云推出的一款全托管、免运维、高弹性的通用PaaS平台。SAE提供了无门槛的容器化、主流微服务和Job任务的全托管,以及多语言监控的能力,对用户来说,是一款技术门槛更低、迁移改造更简单的Serverless平台。通过本实验,将带大家亲......
  • 【LEETCODE】​​1053. 交换一次的先前排列​
    1053.交换一次的先前排列难度中等95给你一个正整数数组 arr(可能存在重复的元素),请你返回可在 一次交换(交换两数字 arr[i] 和 arr[j] 的位置)后得到的、按字典序排列小于 arr 的最大排列。如果无法这么操作,就请返回原数组。 示例1:输入:arr=[3,2,1]输出:[3,1,2]解释:交换2......
  • windows 10 系统 和 VMware Workstation 虚拟机网络互通设置
    windows10系统和VMwareWorkstation虚拟机网络互通设置 1,虚拟机设置网卡地址网关地址子网掩码2,VMwareWorkstation的编辑-虚拟网络编辑器,单击进入配置,为NAT类型。3,本地笔记本电脑的虚拟网卡配置地址网关掩码4,本地笔记本电脑使用secureCRT和winscp测试,连接和上传文件都OK......
  • Wallys|Wi-Fi 7 SoC chip • Alder / BellsIPQ9574 / IPQ9554 / IPQ9514|IPQ9570 / IP
      Wi-Fi7explainedWiFi7istheupcomingWiFistandard,alsoknownasIEEE802.11beExtremelyHighThroughput(EHT).Itworksacrossallthreebands(2.4GHz,5GHz,and6GHz)tofullyutilizespectrumresources.WhileWiFi6wasbuiltinresponseto......
  • 性能工具之JMeter两个Java API Demo
    概述本文演示两个通过JavaAPI执行JMeter脚本的示例主要功能在线生成jmx脚本(demo1)加载本地已有jmx脚本(demo2)运行多个Sampler将生成的TestPlan存储为.jmx文件执行单机压测将测试执行结果存储为.jtlor.csv文件示例Maven配置为了开始使用JMeterAPI,我们首先需要将它添加到......
  • 联芸mas0902固态使用量产工具的开卡方法,mas0902/mas1102开卡软件教程
    MAS0902是一款固态存储器芯片,一般固态硬盘损坏的情况下才需要量产,在量产过程中,需要从量产部落网下载对应主控型号和闪存颗粒类型的量产工具后,使用量产工具来对芯片进行初始化和测试。以下是MAS0902固态使用量产工具的开卡方法:1.首先,将采用MAS0902芯片的固态硬盘短接ROM孔后,连......
  • java稀疏数组实现实例
    没有原理讲解,仅记录一个实现代码,作为参考和笔记使用如题,稀疏数组仅在原始数组有效数据较少的情况下起压缩空间的作用实现过程:首先为了方便查看和确认,封装一个打印二维数组的方法publicstaticvoidprintArray(int[][]arrays){for(int[]array:arrays){......
  • 114.二叉树展开为链表 Java
    114.二叉树展开为链表给你二叉树的根结点root,请你将它展开为一个单链表:展开后的单链表应该同样使用TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。展开后的单链表应该与二叉树先序遍历顺序相同。示例1:输入:root=[1,2,5,3,4,null,6]输出......