zoukankan      html  css  js  c++  java
  • JVM类加载器(二)

    一个类加载器对象主要用于负责加载类,当我们将一个字符串形式的类名作为参数,传给类加载器的方法去加载类的时候,类名必须满足Java所规定的二进制名字。什么是二进制名字呢?比如下面几个例子:

    • java.lang.String
    • javax.swing.JSpinner$DefaultEditor
    • java.security.KeyStore$Builder$FileBuilder$1
    • java.net.URLClassLoader$3$1

    其中,上面的第三和第四个例子可能比较难懂。第三个例子代表KeyStore内部类的Builder的第一个匿名内部类(KeyStore$Builder$FileBuilder$1);同理第四个例子代表URLClassLoader类的第三个匿名内部类的第一个内部类(URLClassLoader$3$1)。

    每个Class对象都会包含对定义它的类加载器对象的引用,但数组的Class对象并不是类加载器创建的,而是Java虚拟机运行期根据需要动态生成的。对于数组类型的加载器,与数组中元素的类型是一样的,如果元素是原生类型,那么这个元素及其对应的数组类型,都没有类加载器。

    我们来看下面这个例子:

    package com.leolin.jvm;
    
    public class MyTest15 {
        public static void main(String[] args) {
            String[] strings = new String[2];
            System.out.println(strings.getClass().getClassLoader());
            MyTest15[] myTest15s = new MyTest15[2];
            System.out.println(myTest15s.getClass().getClassLoader());
            MyTest15[][] myTest15s_1 = new MyTest15[2][2];
            System.out.println(myTest15s_1.getClass().getClassLoader());
            int[] ints = new int[2];
            System.out.println(ints.getClass().getClassLoader());
        }
    }
    

      

    下面的输出证明我们之前的论述:

    null
    sun.misc.Launcher$AppClassLoader@7b7035c6
    sun.misc.Launcher$AppClassLoader@7b7035c6
    null
    

      

    有一点我们需要注意,即便数组的类型有时候可以获取到类加载器,但这个类型并不是加载进来的,而是JVM生成的。

    下面,我们尝试去编写一个类加载器MyTest16:

    package com.leolin.jvm;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    
    public class MyTest16 extends ClassLoader {
        private String classLoaderName;//加载器名字,仅仅是标识性作用
        private String fileExtension = ".class";
    
        public MyTest16(String classLoaderName) {
            super();//将系统类加载器当做该类加载器的父加载器
            this.classLoaderName = classLoaderName;
        }
    
        public MyTest16(ClassLoader parent, String classLoaderName) {
            super(parent);//显式指定该类加载器的父加载器
            this.classLoaderName = classLoaderName;
        }
    
    
        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            //读取class文件的字节数组
            System.out.println("findClass invoked:" + className);
            System.out.println("class loader name:" + this.classLoaderName);
            byte[] data = this.loadClassData(className);
            return super.defineClass(className, data, 0, data.length);
        }
    
        private byte[] loadClassData(String className) {
            InputStream is = null;
            byte[] data = null;
            ByteArrayOutputStream baos = null;
            className = className.replace(".", File.separator);
            try {
                is = new FileInputStream(new File(className + this.fileExtension));
                baos = new ByteArrayOutputStream();
                int ch = 0;
                while ((ch = is.read()) != -1) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                try {
                    is.close();
                    baos.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
            System.out.println("class:" + clazz.hashCode());
            Object object = clazz.newInstance();
            System.out.println(object);
        }
    
    
    }
    

      

    上面的代码,我们重写了findClass方法,这个方法会打印我们要求加载的类名,以及本身的类加载器名字 ,然后会调用loadClassData获取对应的class文件的字节数组。然后我们在main方法中又调用加载器的loadClass方法,并传入类名,这个方法就是帮我们加载类的方法,我们加载的是很早之前所编写的MyTest1类。于是我们执行上面的代码,得到如下的输出:

    class:576153008
    com.leolin.jvm.MyTest1@3a0aaa10
    

      

    很奇怪的是,MyTest1加载成功了,但是我们在findClass方法里所编写的打印却没有输出。这是因为当loader1要去加载MyTest1时,会先将加载任务委托给它的父加载器,也就是系统加载器。系统加载器加载MyTest1成功,也就无须loader1执行findClass方法,将class文件转换成字节数组,将通过defineClass生成对应的Class对象。从这个意义上来说,系统类加载器是MyTest1的定义类加载器,而系统类加载器和MyTest16是MyTest1的初始类加载器。

    为了让MyTest16真正去加载MyTest1,我们要稍作改动,我们给MyTest16新增一个path的成员变量,可以指定一个路径,去读取路径下的类文件:

    package com.leolin.jvm;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    
    public class MyTest16 extends ClassLoader {
        private String classLoaderName;//加载器名字,仅仅是标识性作用
        private String path;
        private String fileExtension = ".class";
    
        public MyTest16(String classLoaderName) {
            super();//将系统类加载器当做该类加载器的父加载器
            this.classLoaderName = classLoaderName;
        }
    
        public MyTest16(ClassLoader parent, String classLoaderName) {
            super(parent);//显式指定该类加载器的父加载器
            this.classLoaderName = classLoaderName;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            //读取class文件的字节数组
            System.out.println("findClass invoked:" + className);
            System.out.println("class loader name:" + this.classLoaderName);
            byte[] data = this.loadClassData(className);
            return super.defineClass(className, data, 0, data.length);
        }
    
        private byte[] loadClassData(String className) {
            InputStream is = null;
            byte[] data = null;
            ByteArrayOutputStream baos = null;
            className = className.replace(".", File.separator);
            try {
                is = new FileInputStream(new File(this.path + className + this.fileExtension));
                baos = new ByteArrayOutputStream();
                int ch = 0;
                while ((ch = is.read()) != -1) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                try {
                    is.close();
                    baos.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            loader1.setPath("C:\Users\admin\Desktop\");
            Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
            System.out.println("class:" + clazz.hashCode());
            Object object = clazz.newInstance();
            System.out.println(object);
        }
    }
    

      

    我们让loader1去桌面读取类文件,同时,我们也要复制我们的类路径到桌面,并将我们工程底下的MyTest1的class文件删除。这样当loader1委托系统加载器加载MyTest1时,系统加载器无法在当前工程的classpath下找到MyTest1,才会将任务回传给loader1,由loader1调用重写的findClass方法去寻找。

    运行上面的代码,得到如下输出:

    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader1
    class:1796456726
    com.leolin.jvm.MyTest1@5de9c245
    

      

    可以看到,现在确实是由我们所定义的类加载器加载的MyTest1。但是如果我们重新编译整个项目,MyTest1又会由系统加载器去加载,我们重写的findClass依旧不会执行。

    我们删除MyTest1的class文件,并将main方法修改如下:

    public static void main(String[] args) throws Exception {
    	MyTest16 loader1 = new MyTest16("loader1");
    	loader1.setPath("C:\Users\admin\Desktop\");
    	Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
    	System.out.println("class:" + clazz.hashCode());
    	Object object = clazz.newInstance();
    	System.out.println(object);
    
    	MyTest16 loader2 = new MyTest16("loader2");
    	loader2.setPath("C:\Users\admin\Desktop\");
    	Class<?> clazz2 = loader2.loadClass("com.leolin.jvm.MyTest1");
    	System.out.println("class:" + clazz2.hashCode());
    }
    

      

    运行结果如下:

    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader1
    class:942306880
    com.leolin.jvm.MyTest1@6bb9808e
    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader2
    class:1770329462
    

      

    可以看到,MyTest1分别在loader1和loader2加载了两次,而且clazz和clazz2的hashCode也不一样。这似乎和我们之前提到的一个类只能加载一次互相矛盾了,其实这里还涉及到一个类加载器命名空间的问题。

    命名空间

    每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。

    在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

    在不同的命名空间中,有可能出现类的完整名字相同的两个类。

    之前的loader1和loader2分别为两个不同的命名空间,所以允许MyTest1分别出现在这两个命名空间中。

    我们将loader2的声明改为:

    MyTest16 loader2 = new MyTest16(loader1, "loader2");
    

      

    让loader1作为loader2的父加载器,重新执行程序,得到如下输出:

    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader1
    class:500265006
    com.leolin.jvm.MyTest1@211beb4d
    class:500265006
    

      

    类的卸载

    当MySample被加载、连接和初始化之后,它的生命周期就开始了。当MySample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,MySample类在方法区内的数据也会被卸载,从而结束MySample类的生命周期。
    一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
    由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象是可触及的。
    由用户自定义的类加载器所加载的类是可以被卸载的。

    现在,我们来展示一下类的卸载,这里要认识一个JVM参数,-XX:+TraceClassUnloading,用来追踪被卸载的类,配置好参数后,我们修改MyTest16的main方法如下:

    public static void main(String[] args) throws Exception {
    	MyTest16 loader1 = new MyTest16("loader1");
    	loader1.setPath("C:\Users\admin\Desktop\");
    	Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
    	System.out.println("class:" + clazz.hashCode());
    	Object object = clazz.newInstance();
    	System.out.println(object);
    	System.out.println("---------");
    	loader1 = null;
    	clazz = null;
    	object = null;
    	System.gc();
    	loader1 = new MyTest16("loader1");
    	loader1.setPath("C:\Users\admin\Desktop\");
    	clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
    	System.out.println("class:" + clazz.hashCode());
    	object = clazz.newInstance();
    	System.out.println(object);
    }
    

      

    运行代码,得到如下输出:

    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader1
    class:1807319182
    com.leolin.jvm.MyTest1@5fa721e2
    ---------
    [Unloading class com.leolin.jvm.MyTest1]
    findClass invoked:com.leolin.jvm.MyTest1
    class loader name:loader1
    class:1142817658
    com.leolin.jvm.MyTest1@e766186
    
    Process finished with exit code 0
    

      

    可以看到当我们将MyTest1所对应Class对象不再被引用时,执行GC,虚拟机会卸载MyTest1对应的Class对象。还要一个办法是在GC后休眠足够长的一段时间,然后用用工具jvisualvm->查看java进程->监视,里面可以看到被卸载的类。

  • 相关阅读:
    Nginx系列p5:进程管理(信号)
    Nginx系列p4:进程结构
    one_day_one_linuxCmd---crontab 命令
    Nginx系列p3:实现一个具有缓存功能的反向代理服务器
    Nginx系列p2:重载,热部署,日志分割
    Nginx系列p1:安装
    数据链路层
    物理层习题与真题
    物理层
    计算机网络体系结构习题与真题
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/12821979.html
Copyright © 2011-2022 走看看