Java类加载机制

一:概述

    每个编写的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() {
     //加载/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 binary name.
     * 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) loadClass(name,
     * false)}.
     *
     * @param  name
     *         The binary name of the class
     *
     * @return  The resulting Class 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类加载机制)