美文网首页
Java类加载机制

Java类加载机制

作者: 涂豪_OP | 来源:发表于2018-08-04 20:13 被阅读70次

    一:概述

        每个编写的java文件,都存储着需要执行的逻辑;这些java文件经过编译器编译成class文件,当需要使用某个类的时候,虚拟机就会加载他的class文件并创建响应的Class对象;将class文件加载到虚拟机内存的过程叫做类加载;其过程如下(盗图): 类加载过程
        类加载过程包括以下五大步骤:

        1.加载:通过类的完全限定名(包名 + 类名)查找此类的class文件,并创建一个Class对象。
        2.验证:校验class文件的正确性,class文件加载后,最基本的是不能破坏虚拟机的正常运行,这就需要校验;校验包括文件格式(魔数)校验、元数据校验、字节码校验、符号引用校验。
        3.准备:为类变量(static)分配内存空间(在方法区/元数据区分配),并对他们进行初始化,是初始化,不是赋代码里面的值;final类型修饰的static变量除外,这种类型的数据在编译期就分配了空间。
        4.解析:将常量池中的符号引用转换成直接引用。符号引用是用一组符号来引用目标,这个符号可以是任何字面量;而直接引用是指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。
        5.类加载的最后阶段,初始化该类。若该类有父类,则对父类进行初始化。

    二:类加载器类型

        类加载器的任务是根据一个全限定类名读取目标class文件的二进制流到虚拟机中,然后创建Class对象。虚拟机提供了三种类加载器:引导加载器(Bootstrap加载器)、扩展加载器(Extension)和系统加载器(System加载器,也称为应用类加载器)。

    启动类加载器(Bootstrap类加载器):
        启动类加载器主要用于加载虚拟机本身需要用到的类,他是虚拟机自身的一部分。他用于%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等,另外可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中;注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

    扩展类加载器(Extension类加载器):
        它负责加载加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录,开发者可以直接使用标准扩展类加载器。下面的代码能都查看扩展类加载器记载的类路径:

    //ExtClassLoader类中获取路径的代码
    private static File[] getExtDirs() {
         //加载<JAVA_HOME>/lib/ext目录中的类库
         String s = System.getProperty("java.ext.dirs");
         File[] dirs;
         if (s != null) {
             StringTokenizer st =
                 new StringTokenizer(s, File.pathSeparator);
             int count = st.countTokens();
             dirs = new File[count];
             for (int i = 0; i < count; i++) {
                 dirs[i] = new File(st.nextToken());
             }
         } else {
             dirs = new File[0];
         }
         return dirs;
    }
    

    系统类加载器:
        系统类类加载器也称为应用类加载器;他负责加载系统类路径java -classpath或者-D java.class.path下的类库,也是我们经常用到的classpath的路径;一般情况下,该类加载器是程序中默认的类加载器。通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器。

        在日常开发中,类的加载几乎全部右上面三种类加载器配合执行。必要的时候,我们也可以自定义自己的类加载器。类的加载是按需加载,只有真正用到该类是,才会把该类加载到内存,并创建相应的Class对象;而加载的过程通过双亲委派的方式加载。

    三:双亲委派模式

        双亲委派模式要求除了顶级的加载器之外,其他的加载器都要有父类加载器,这个父类的意思不是java里面的类继承,而是采用组合关系来复用父类加载器的代码,类加载器的关系图如下(盗图): 类加载器关系

        双亲委派的工作原理是:如果一个类加载器收到了一个加载类的请求,他并不会自己马上去加载该类,而是委托给他爹去加载;如果他爹还存在父类加载器;那么就委派他爷爷去加载,如此类推,一直委托到顶级加载器;如果他爹能加载,那么就加载吧;如果他爹加载不了,就只能自己动手丰衣足食了,这就是双亲委派。为什么要采用这种模式呢?

        双亲委派的优势:
        采用这种模式的好处是java类随着他的类加载器一起天然具备了一种带有优先级的层次关系,通过这种层次关系,可以避免类的重复加载;当父类加载了该类,那么子类就没必要再加载该类;其次是考虑到安全因素,java核心api肯定是不能随意被串改的,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

        下图是类加载器的关系图(盗图): 类加载器继承关系
        

    四:源码分析

    ClassLoader是一个抽象类,先来看下API对该类的描述: ClassLoader

        接下来看下loadClass方法,看他是怎么实现双亲委派的:

        /**
         * Loads the class with the specified <a href="#name">binary name</a>.
         * This method searches for classes in the same manner as the {@link
         * #loadClass(String, boolean)} method.  It is invoked by the Java virtual
         * machine to resolve class references.  Invoking this method is equivalent
         * to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,
         * false)</tt>}.
         *
         * @param  name
         *         The <a href="#name">binary name</a> of the class
         *
         * @return  The resulting <tt>Class</tt> object
         *
         * @throws  ClassNotFoundException
         *          If the class was not found
         */
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    

        name就是类的全限定名,loadClass调用了重载的loadClass方法,第二个参数是指是否解析加载后生成的Class对象,默认不解析:

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            //给类加载器加锁;getClassLoadingLock
            //的作用返回一个锁对象,具体过程一会分析
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                //首先调用findLoadedClass检查目标类是否已经被加载过
                Class<?> c = findLoadedClass(name);
    
                //如果没有加载,那么进去加载吧
                if (c == null) {
                    //获取最准确的可用系统计时器的当前值,以毫微秒为单位。
                    long t0 = System.nanoTime();
    
                    try {
                        //如果父加载器不为空,那么委托父加载器去加载,递归调用
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            //否则就委托启动加载器去加载
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    //如果父类加载器和启动加载器都加载不了,那么只能自己动手丰衣足食了
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        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();
                    }
                }
    
                //如果需要解析Class对象的话,那就解析他
                if (resolve) {
                    resolveClass(c);
                }
    
                //最终返回Class对象
                return c;
            }
        }
    

        双亲委派就是这么实现的,每个类加载器都持有父加载器的引用;每次加载的时候就递归调用父加载器的findClass方法去加载,一直委托到启动加载器,如果启动记载其都加载不了,那么自己加载;上面的方法有几个难点,下面一一分析:

    //参数className就是要加载的类
    protected Object getClassLoadingLock(String className) {
            //将当前类加载器赋值给lock
            Object lock = this;
    
            //parallelLockMap是一个ConcurrentHashMap,他是在类加载器
            //被创建的时候初始化,不过要不要初始化是有条件的,如果该类加载器
            //不具备并行加载的能力,那么就不初始化;一旦初始化了,说明该类加
            //器具有并行加载的能力。这个时候就要去parallelLockMap找与传进
            //来的类的对应的锁对象,这些类和他对应的锁都存在了这个集合里面
            if (parallelLockMap != null) {
                //创建一个新的锁,Object类型
                Object newLock = new Object();
    
                //putIfAbsent是ConcurrentHashMap的方法,线程安全,跟HashMap
                //的put方法类似,就是往集合里面存入键值对,若干键存在,那么就更新
                //并返回老的value,否则就插入并返回空;
                lock = parallelLockMap.putIfAbsent(className, newLock);
    
                //如果ConcurrentHashMap里面没有这个key,那么
                //lock就是空,此时就刚刚创建的newLock赋值给lock
                if (lock == null) {
                    lock = newLock;
                }
            }
            //返回锁对象
            return lock;
        }
    

        综上分析,getClassLoadingLock就是获取一个和待加载类绑定的锁对象。接下来分析findLoadedClass,这个方法的作用是判断待加载的类是否已经被加载过,如果加载过,那么返回这类的Class对象:

    protected final Class<?> findLoadedClass(String name) {
        //对类名进行校验,比如不能为空等
        if (!checkName(name))
            //如果类名不符合要求,那么返回空
            return null;
        //调用findLoadedClass0
        return findLoadedClass0(name);
    }
    

        findLoadedClass比较简单,首先校验类名是否合法,接着调用findLoadedClass0,这个方法是native方法,看不到代码,就此打住。
        如果待加载的类没有被加载过,父加载器也加载不了,那么就自己调用findClass去加载类;虽然类加载类是从loadClass开始的,但是实际上加载类是在findClass方法里面进行的,一般自定义类加载器是重写findClass方法,而不是loadClass方法,因为loadClass方法已经实现了双亲委派的逻辑,这个逻辑我们不需要重写,所以我们只需要重写findClass方法即可。这个类是空实现,留待各个加载器的子类自己实现。

    //空实现;上面说了,类加载是在findClass里面进行
    //的,这里使用空实现,给了各个类加载器自己发挥的空间
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
    }
    

        除了上面提到的几个方法,还有两个方法非常重要:defineClass和resolveClass。defineClass方法的作用是将byte字节流解析成JVM能够识别的Class对象,ClassLoader中已实现该方法逻辑,无需我们自己重写(自己重写的要求有点高);resolveClass的作用是解析Class对象,也可以理解成类加载过程中的链接那一步骤,这两个方法都很难,不做分析,我们开发过程中也不太会去重写这两个方法。

    五:Demo验证

        下面通过一个demo来理解下各个类加载器之间的关系:

    public class TestLoader extends ClassLoader{
        public static void main(String[] args) {
            TestLoader tl = new TestLoader();
            
            //自定义类加载器的父类加载器
            System.out.println("自定义类加载的父类加载器 : " + tl.getParent());
            
            System.out.println("系统默认加载器 : " + ClassLoader.getSystemClassLoader());
            
            System.out.println("系统默认类加载器的父类加载器 : " + 
                      ClassLoader.getSystemClassLoader().getParent());
            
            System.out.println("扩展类加载器的父类加载器 : " + 
                     ClassLoader.getSystemClassLoader().getParent().getParent());
            
            System.out.println("系统默认类加载器的父类 : " + 
                     ClassLoader.getSystemClassLoader().getParent().getClass().getSuperclass().getName());
        }
    }
    

        输出结果如下:

    自定义类加载的父类加载器 : sun.misc.Launcher$AppClassLoader@4e25154f
    系统默认加载器 : sun.misc.Launcher$AppClassLoader@4e25154f
    系统默认类加载器的父类加载器 : sun.misc.Launcher$ExtClassLoader@33909752
    扩展类加载器的父类加载器 : null
    系统默认类加载器的父类加载器 : java.net.URLClassLoader
    

        可以看到,自定义类加载器的父加载器是系统默认加载器AppClassLoader;AppClassLoader的父加载器是ExtClassLoader;而ExtClassLoader的父加载器是空;另外注意下,ExtClassLoader的父类(不是父加载器)是URLClassLoader,乱入一个URLClassLoader是什么鬼?其实URLClassLoader是ClassLoader的子类,他重写了findClass和defineClass方法,既然系统默认类加载器都继承自该类,我们自定义类加载器有什么理由不去继承URLClassLoader而去继承CloadClass呢?

      类的唯一性
        在刚学java的时候,我们一般都认为包名 + 类名就能唯一确定一个类,但是这种说法是不严谨的,请看例子;创建同一类型的加载器的两个对象去加载同一个类:

    public class TestLoader extends ClassLoader{
        //该类加载器查找的路径
        private String dir;
    
        //构造函数
        public TestLoader(String dir) {
            this.dir = dir;
        }
    
        public static void main(String[] args) throws ClassNotFoundException {
            //注意,类加载器加载的是class文件,所以用eclipse测试的时候
            //要把src改成bin,要不然会抛出ClassNotFoundException异常
            String rootDir = "/Users/tushihao/eclipse-workspace/Test/bin/";
    
            //创建两个自定义的类加载器,对象不同,但类型一样
            TestLoader t1 = new TestLoader(rootDir);
            TestLoader t2= new TestLoader(rootDir);
    
            //通过两个类加载器去加载同一个类(传入类名和包名),这里可能会抛异常
            try {
                Class<?> c1 = t1.loadClass("testclassloader.Test");
                Class<?> c2 = t2.loadClass("testclassloader.Test");
                System.out.println(c1.hashCode());
                System.out.println(c2.hashCode());
            } catch (ClassNotFoundException e) {
                System.out.println("class not found");
            }
    }
    

        输出结果如下:

    865113938
    865113938
    

        卧槽,不同类加载器加载同一个类,结果相同,这脸打的好痛。什么原因导致的呢?还记得loadClass的代码吗?

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            //给类加载器加锁;getClassLoadingLock
            //的作用返回一个锁对象,具体过程一会分析
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                //首先调用findLoadedClass检查目标类是否已经被加载过
                Class<?> c = findLoadedClass(name);
    
            ......
    

        从代码可以看出,t1去加载Test的class文件后,会把加载的结果缓存起来;t2再去加载;t2加载的第一步是看缓存里面有没有Test,如果有,就直接返回;否则就自己去找,这里并没有判断类加载器是不是同一个,所以才出现了上面的结果。要想不查缓存,要么重写loadClass方法,要么重写findClass,然后直接去调用findClass,因为findClass是不会去查缓存的;考虑到重写loadClass还要自己写一套维持双亲委派的逻辑,不值当,所以这里选择直接调用findClass,这样的话就必须重写这个方法了,因为ClassLoader的findClass是空实现,不重写就会抛出ClassNotFoundException:

    //重写findClass方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //通过getClassData去读取class文件到byte数组
        byte[] classData = getClassData(name); 
    
        //如果没读到肯定要抛出异常给你尝尝
        if (classData == null) { 
            throw new ClassNotFoundException(); 
        } 
        else { 
            //读到了的话就调用系统的defineClass方法构建Class对象
            return defineClass(name, classData, 0, classData.length); 
        } 
    }
    
    //读取class文件的数据
    private byte[] getClassData(String className) { 
        String path = classNameToPath(className); 
        try { 
            InputStream ins = new FileInputStream(path); 
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
            int bufferSize = 4096; 
            byte[] buffer = new byte[bufferSize]; 
            int bytesNumRead = 0; 
            while ((bytesNumRead = ins.read(buffer)) != -1) { 
                baos.write(buffer, 0, bytesNumRead); 
            } 
            return baos.toByteArray(); 
         } catch (IOException e) { 
            e.printStackTrace(); 
         } 
         return null; 
    } 
    
    private String classNameToPath(String className) { 
        return dir + File.separatorChar 
            + className.replace('.', File.separatorChar) + ".class"; 
    } 
    

        上面就重写了findClass方法,下面把main方法改成下面这样:

    public static void main(String[] args) throws ClassNotFoundException {
            //注意,类加载器加载的是class文件,所以用eclipse测试的时候
            //要把src改成bin,要不然会抛出ClassNotFoundException异常
            String rootDir = "/Users/tushihao/eclipse-workspace/Test/bin/";
    
            //创建两个自定义的类加载器
            TestLoader t1 = new TestLoader(rootDir);
            TestLoader t2= new TestLoader(rootDir);
    
            //通过两个类加载器去加载同一个类(传入类名和包名),这里可能会抛异常
            try {
                Class<?> c1 = t1.findClass("testclassloader.Test");
                Class<?> c2 = t2.findClass("testclassloader.Test");
                System.out.println(c1.hashCode());
                System.out.println(c2.hashCode());
            } catch (ClassNotFoundException e) {
                System.out.println("class not found");
            }
    }
    

        输出结果如下:

    1975012498
    1808253012
    

        可以看到,不同的加载器加载同一个类,加载的结果就不一样了,终于不被打脸了。所以,根据包名 + 类名,不一定能唯一确定一个类。

    六:自定义加载器的必要性

        通过前面的Demo可知,要自定义一个类加载器,可以继承ClassLoader或者URLClassLoader;如果继承自ClassLoader,那么需要自己重写findClass方法,也就是自己去找指定位置的class文件,把数据读出来(byte数组类型),转换成Class对象(转换过程已经在系统方法ClassLoader中实现,无需重写);如果继承自URLClassLoader,那么连findClass方法都不用重写了(当然,也可以重写);那么自定义类加载器的意义何在?
        1.当class文件不在classpath下时,系统类加载器无法找到该class文件,此时就需要我们自己写一个类加载器去加载指定路径下的class文件并创建Class对象了。
        2.当一个class文件是通过网络传输过来时,此class文件可能被加密,此时需要先对此class文件进行解密才能被使用,这就需要自定义一个类加载器进行解密,然后加载到内存去。
        3.当实现热部署功能时(一个class文件通过不同的类加载器产生不同的class对象从而实现热部署),需要自定义一个类加载器,这是很常见的需求。

        下面再看一个完整的读取网络传输的class文件并创建对象的demo:

    public class NetClassLoader extends ClassLoader {
        //网络上class文件的URL
        private String url;
        
        public NetClassLoader(String url) {
            super();
            this.url = url;
        }
    
        public static void main(String[] args) {
            
        }
        
        //重写findClass方法
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] data = getClassDataFromNet(name);
            //找不到文件就死给你看
            if(data == null) {
                throw new ClassNotFoundException();
            }else {
                //调用系统方法创建Class对象
                return defineClass(name,data, 0, data.length);
            }
        }
    
        private byte[] getClassDataFromNet(String className) {
            
            //获取网络上的class文件的路径
            String path = classNameToPath(className);
            
            //下载class文件并转化成byte数组
            try {
                URL url = new URL(path);
                InputStream is = url.openStream();
                ByteArrayOutputStream  bas = new ByteArrayOutputStream();
                int buffer = 4096;
                
                byte[] bf = new byte[buffer];
                int readNum = 0;
                
                while((readNum = is.read(bf)) != -1) {
                    bas.write(bf,0,readNum);
                }
                
                //解密
                decrypt(bas);
                return bas.toByteArray();
            }catch (Exception e) {
                // TODO: handle exception
            }
            return null;
        }
    
        //解密方法
        private void decrypt(ByteArrayOutputStream bas) {
            //假装我已经解密了
        }
    
        private String classNameToPath(String className) {
            return url + "/" + className.replace(".", "/") + ".class";
        }
    }
    

    摘自:https://blog.csdn.net/javazejian/article/details/73413292 (略有改动)

    相关文章

      网友评论

          本文标题:Java类加载机制

          本文链接:https://www.haomeiwen.com/subject/nclmvftx.html