JVM自定义类加载器及注意问题

JVM自定义类加载器及问题

在看神书《深入理解Java虚拟机》时,看到类加载部分,对如何自定义一个类加载器来,加载自己想要的加载的类产生了兴趣,所以研究了一下。参考了一下网上的其他的文章 ,记录一下。

  1. 首先为什么要自定义类加载器呢

    • 我们需要的类不一定存放在已经设置好的classpath下(由系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader

    • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些 加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。

    • 可以定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。

    • 一些软件设计时为了实现更好的功能和设计思想。如tomcat

      tomcat使用自定义类加载器的原因:

      1. 要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
      2. 由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
      3. 有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
      4. 部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。
  2. 类是如何被加载到JVM的?

    在jdk中,是通过ClassLoader来加载jdk的系统类和用户类路径下(classpath)的类的。
    在ClassLoader中的loadClass方式,实现了类的加载逻辑,即非常有名的双亲委类加载模式。

    在这里插入图片描述

    工作过程如下:

    • 当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则 直接返回原来已经加载的类。

    • 如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。

    • 如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。

    代码如下,可简单看下:

    protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
            //检查指定类是否被当前类加载器加载过
            Class c = findLoadedClass(name);
            if( c == null ){//如果没被加载过,委派给父加载器加载
                try{
                    if( parent != null )
                        c = parent.loadClass(name,resolve);
                    else 
                        c = findBootstrapClassOrNull(name);
                }catch ( ClassNotFoundException e ){
                    //如果父加载器无法加载
                }
                if( c == null ){//父类不能加载,由当前的类加载器加载
                    c = findClass(name);
                }
            }
            if( resolve ){//如果要求立即链接,那么加载完类直接链接
                resolveClass();
            }
            //将加载过这个类对象直接返回
            return c;
        }
    

    如果想要加载的类不是jdk的系统类或者classpath下的用户类,即上面第1条中说的情况,那么类加载器是无法记载的,就会进入如下逻辑。

    //父类不能加载,由当前的类加载器加载
    if( c == null ){
        c = findClass(name);
    }
    

    附上findClass方法

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

    从此段代码可以看出,如果没有重写此方法,那么会直接抛出ClassNotFoundException异常。类加载失败。所以这时候如果要加载这个类,那么就需要重写findClass方法。

  3. 如何重写findClass来实现一个自己的类加载器?

    1. 首先创建一个非jdk的系统类路径和非classpath路径和的外部class文件。

      为了方便,在D盘根目录下创建一个.java文件。

      注意不要在你的ide中创建,否则会在运行程序的时候被编译到用户类路径(classpath)下,那么还是会被AppClassLoader加载,除非创建此类生成此类class文件后,删掉此类的.java和.class文件,再运行自定义类加载器

      // 定义一个包路径
      package com.jvm.test;
      
      /**
       * @author: yhl
       * @DateTime: 2019/12/5 9:41
       * @Description: 定义类
       */
      public class People {
      
      }
      
      

      在此目录下shift+鼠标右击,选择打开cmd(win10为Powershell)。运行java命令生成.class文件。

      javac -encoding utf-8 .\Test.java
      

      查看此目录,已经生成了Test.class文件

    2. 创建自己的类加载器加载Test.class文件。 代码如下:

    /**
     * Author: yhl
     * DateTime: 2019/12/4 21:12
     * Description: 自定义类加载
     *              需要继承ClassLoader,重写findClass
     */
    public class TestClassLoader extends ClassLoader {
    
        /**
         *
         *
         *  此路径即时想要加载的外部class路径
         */
        private String filePath;
    
        TestClassLoader(String filePath){
            this.filePath = filePath;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
            File file = new File(filePath);
            try {
                byte[] bytes = getClassBytes(file);
                // 如果读取的.class文件为空,则抛出ClassNotFoundException异常
                if (bytes == null) {
                    throw new ClassNotFoundException();
                }
                //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
                Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
                return c;
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return super.findClass(name);
        }
    
        private byte[] getClassBytes(File file) throws Exception {
            // 这里要读入.class文件的字节,因此要使用字节流
            FileInputStream fis = new FileInputStream(file);
            FileChannel fc = fis.getChannel();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            WritableByteChannel wbc = Channels.newChannel(baos);
            ByteBuffer by = ByteBuffer.allocate(1024);
            while (true) {
                int i = fc.read(by);
                if (i == 0 || i == -1)
                    break;
                by.flip();
                wbc.write(by);
                by.clear();
            }
            fis.close();
            return baos.toByteArray();
        }
    
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            // 创建自定义加载器实例
            TestClassLoader mcl = new TestClassLoader("D:/Test.class");
    
            // classPackage为自定义的class中的包路径
            String classPackage = "com.jvm.test.Test";
            // 通过自定义类加载器加载此class,
            // Class<?> obj = Class.forName(classPackage, true, mcl).newInstance();
            Object obj = mcl.loadClass(classPackage).newInstance();
            // 打印加载的类对象
            System.out.println(obj);
            // 打印加载此类的类加载器
            System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器
        }
    }
    

    打印结果如下:

    com.jvm.test.Test@63961c42
    com.boot.demo.jvm.TestClassLoader@85ede7b
    

    TestClassLoader即为我们自定义的类加载器。

  4. 注意事项

    在尝试自定义的过程中,遇到了一些问题,花了些许时间,在此记录一下。

    1.加载类的必须是非jdk系统类路径和用户类路径下(classpath)的class文件,否则还是会使用AppClassLoader来加载,不是自定义的类加载。所以在上面第3条第i点的注意事项很重要。

    2.其实重写ClassLoader中的loadClass也可以使用自定义类加载器,而且不用考虑上面第1条要注意的问题。但是,这样会破坏双亲委派模型,是不可取的。如下面代码

    /**
     * @author: yhl
     * @DateTime: 2019/12/4 15:26
     * @Description:
     */
    public class ClassLoaderTest extends ClassLoader{
        public static void main(String[] args) throws Exception {
    
            /**
             * 重写loadClass,违背了双亲委派原则。
             * 因为双亲委派的逻辑是在loadClass中通过递归实现
             * 正确操作为重写findClass
             */
            ClassLoader load = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
    
                        if (resourceAsStream == null)
                            return super.loadClass(name);
    
                        byte[] b = new byte[resourceAsStream.available()];
                        resourceAsStream.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException();
                    }
                }
            };
    
    
            ClassLoader find = new ClassLoader() {
                @Override
                protected Class<?> findClass(String name) throws ClassNotFoundException {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
                    if (resourceAsStream == null) {
                        return super.findClass(name);
                    } else {
                        byte[] b = new byte[0];
                        try {
                            b = new byte[resourceAsStream.available()];
                            resourceAsStream.read(b);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return defineClass(name, b, 0, b.length);
                    }
                }
            };
    
            // 重写loadClass,破坏双亲委派原则,但会使用自定义的类加载器
            Object loadObject = load.loadClass("com.boot.demo.jvm.ClassLoaderTest").newInstance();
            System.out.println("load:"+ loadObject.getClass().getClassLoader());
            System.out.println(loadObject instanceof com.boot.demo.jvm.ClassLoaderTest);
            System.out.println("------------------------------------------------------");
            // 重写findClass,运行时由于要加载的类在classpath下已经编译好,则会用AppClassLoader来加载,
            // 如果要使用自定义累加器的加载,则应该使用类路径classpath外的一个class文件,并且类路径下不能包含此class
            // 才能使用到自定义的类加载器
            Object findObject = find.loadClass("com.boot.demo.jvm.ClassLoaderTest").newInstance();
            System.out.println("find:"+ findObject.getClass().getClassLoader());
            System.out.println(findObject instanceof com.boot.demo.jvm.ClassLoaderTest);
    }
    
    

    打印结果如下

    load:com.boot.demo.jvm.ClassLoaderTest$1@65b54208
    false
    ------------------------------------------------------
    find:sun.misc.Launcher$AppClassLoader@18b4aac2
    true
    
发布了23 篇原创文章 · 获赞 18 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_35457078/article/details/103403148