1 前语

咱们往常开发中,都会布置开发的项目或许本地运转main函数之类的来发动程序,那么咱们项目中的类是怎么被加载到JVM的,加载的机制和完结是什么样的,本文给咱们简略介绍下。

2 类加载运转全进程

当咱们用java指令运转某个类的main函数发动程序时,首要需求经过类加载器把主类加载到JVM,经过Java指令履行代码的大体流程如下

源码剖析JVM类加载机制

从流程图中能够看到类加载的进程首要是经过类加载器来完结的,那么什么是类加载器呢?

3 类加载器

3.1 什么是类加载器

类加载器担任在运转时将Java类动态加载到JVM(Java 虚拟机)。此外,它们是JRE(Java运转时环境)的一部分。所以由于类加载器,JVM不需求知道底层文件或文件系统来运转Java程序。

Java类加载器的作用是寻找类文件,然后加载Class文件加载到内存,并对数据进行校验、转换解析和初始化,终究形成能够被虚拟机直接运用的Java类型。

3.2 类加载器品种

3.2.1 发动类加载器(Bootstrap ClassLoader)

它首要担任加载JDK内部类,一般是rt.jar和其他坐落$JAVA_HOME/jre/lib目录下的中心库。此外,Bootstrap类加载器充当一切其他ClassLoader实例的父级。

Bootstrap ClassLoader是JVM中心的一部分,是用native引证编写的。它本身是虚拟机的一部分,所以它并不是一个JAVA类,咱们无法直接运用该类加载器。

3.2.2 扩展类加载器(Extension ClassLoader)

担任加载支撑JVM运转的坐落$JAVA_HOME/jre/lib目录下的ext扩展目录中的JAR 类包。咱们能够直接运用这个类加载器。

3.2.3 运用程序类加载器(Application ClassLoader)

担任加载用户类途径(classpath)上的指定类库,首要便是加载你自己写的那些类。一般状况,假如咱们没有自定义类加载器默许便是用这个加载器。

3.2.4 自定义类加载器

经过承继ClassLoader类完结,首要重写findClass办法。

下面经过代码来看下了解不同的类是运用的哪品种加载器来加载的:

System.out.println("Classloader of this class : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader of Logging : " + Logging.class.getClassLoader());
System.out.println("Classloader of String : " + String.class.getClassLoader());
System.out.println("-----------");
System.out.println("Classloader : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent().getParent());

下面是运转成果:

源码剖析JVM类加载机制

经过运转成果,咱们会发现我自定义的当时运转类的类加载器是AppClassLoader,Logging这个类的类加载器是ExtClassLoader,并且类加载器之间是有父子联系相关的。但String的类加载器却为null,ExtClassLoader的父加载器也为null,是意味着String类不是经过类加载器加载的?那假如能够加载它又是怎么被加载的呢?为什么咱们获取不到BootstrapClassLoader呢?后边咱们会进行解读。

3.3 类加载器的机制

上面介绍了都有哪些类加载器,那么一个类是怎么被类加载器加载的,这些类加载器之间又有什么相相联系呢,接下来就介绍下类加载器的机制。

双亲派遣机制

假如一个类加载器收到了类加载的恳求,它首要不会自己去尝试加载这个类,而是把这个恳求派遣给父类加载器去完结,每一个层次的类加载器都是如此,因而一切的加载恳求终究都会传送到顶层的发动类加载器中,只要当父加载器反应自己无法完结这个加载恳求时(它查找的规模没有找到所需的类),子加载器才会尝试自己取加载。

双亲派遣机制说简略点便是:关于每个类加载器,只要父类(顺次递归)找不到时,才自己加载 。

源码剖析JVM类加载机制

3.4 类加载机制的源码完结

参见最开端类运转加载全进程图可知,流程中会创立JVM发动器实例:sun.misc.Launcher。 sun.misc.Launcher初始化运用了单例模式规划,确保一个JVM虚拟机内只要一个sun.misc.Launcher实例。

在Launcher结构办法内部,其创立了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(运用类加载器)。JVM默许运用Launcher的getClassLoader(),这个办法回来的类加载器(AppClassLoader)的实例加载咱们的运用程序。

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        Thread.currentThread().setContextClassLoader(this.loader);
        。。。。。。 //省掉一些不需重视代码
    }

从上面Launcher结构办法的源码中,咱们看到了AppClassLoader和ExtClassLoader这两品种加载器的定义,并且在创立AppClassLoader时将ExtClassLoader设置为父类,也契合上面说的类加载器之间的相关。
可是BootstrapClassLoader依然没有出现,并且也没有给ExtClassLoader设置父加载器,那它又是和ExtClassLoader怎么相关的?下面的双亲派遣机制完结的源码会为咱们解答。

咱们来看下AppClassLoader加载类的双亲派遣机制源码,AppClassLoader的loadClass办法终究会调用其父类ClassLoader的loadClass办法,该办法的大体逻辑如下:

  • 首要查看一下指定名称的类是否现已加载过,假如加载过了,就不需求再加载,直接回来。
  • 假如此类没有加载过,那么,再判别一下是否有父加载器;假如有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);),或许是调用bootstrap类加载器来加载。
  • 假如父加载器及bootstrap类加载器都没有找到指定的类,那么调用当时类加载器的findClass办法,在文件系统本身中查找类,来完结类加载。
  • 假如最终一个子类加载器也无法加载该类,则会抛出 java.lang.NoClassDefFoundError。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查看当时类加载器是否现已加载了该类
            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();
// 都会调用URLClassLoader的findClass办法在加载器的类途径里查找并加载该类
                    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) {
 // 解析、链接指定的Java类
                resolveClass(c);
            }
            return c;
        }
    }

上面便是双亲派遣机制完结原理的源码。从中咱们能够看到有一个逻辑点会调用findBootstrapClassOrNull()这个办法,那么至此,咱们有个疑团也就解开了:ExtClassLoader和BootstrapClassLoader(发动类加载器)便是在这里相关上的。由于ExtClassLoader在定义的时分,没有设置父类加载器(parent),所以履行到了这个逻辑,托付了BootstrapClassLoader进行加载。上面说的类加载器之间层级联系的完结和相关,也是在块逻辑里完结的。从源码这里的逻辑,也契合前面咱们介绍BootstrapClassLoader所说的:Bootstrap类加载器充当一切其他ClassLoader实例的父级。

这个疑团是解开了,可是之前还有一个疑团依然没有阐明,在开端咱们获取不同的类的加载器的时分,String的类加载器是null。在类加载的源码里边,咱们看到了BootstrapClassLoader加载器的获取,为什么获取不到是null呢。这个咱们要看下findBootstrapClassOrNull()这个办法的完结,看看BootstrapClassLoader到底是怎么定义的。

    /**
     * Returns a class loaded by the bootstrap class loader;
     * or return null if not found.
     */
    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;
        return findBootstrapClass(name);
    }
    // return null if not found
    private native Class<?> findBootstrapClass(String name);

经过源码能够看到终究调用了findBootstrapClass这个办法来回来,可是这个办法的修饰符是native,那么就容易理解咱们为什么获取不到这个BootstrapClassLoader了。

3.5 为什么规划双亲派遣

沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便能够防止中心API库被随意篡改 ,防止了恶意代码的注入,安全性的进步和确保。

避免类的重复加载:当父亲现已加载了该类时,就没有必要子ClassLoader再加载一次。假如每个加载器都自己加载,那么可能会出现多个同名类,导致混乱。

3.6 双亲派遣机制的打破

双亲派遣模型很好的处理了各个类加载器加载根底类的统一性问题。即越根底的类由越上层的加载器进行加载。

若加载的根底类中需求回调用户代码,而这时顶层的类加载器无法辨认这些用户代码时,就需求损坏双亲派遣模型了。下面就介绍几种损坏了双亲派遣机制的场景。

3.6.1 JNDI损坏双亲派遣模型

JNDI是Java标准服务,它的代码由发动类加载器去加载,但JNDI需求回调独立厂商完结的代码,而类加载器无法辨认这些回调代码(SPI)。为了处理这个问题,引入了一个线程上下文类加载器(ContextClassLoader)。可经过Thread.setContextClassLoader()设置。运用线程上下文类加载器去加载所需求的SPI代码,即父类加载器恳求子类加载器去完结类加载的进程,而损坏了双亲派遣模型。

3.6.2 Spring损坏双亲派遣模型

Spring要对用户程序进行组织和办理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。

那么Spring是怎么访问WEB-INF下的用户程序呢?——运用线程上下文类加载器

Spring加载类所用的classLoader都是经过Thread.currentThread().getContextClassLoader()获取的。当线程创立时会默许创立一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。运用这个来加载用户程序,即任何一个线程都可经过getContextClassLoader()获取到WebAppclassLoader。

3.6.3 Tomcat损坏双亲派遣机制

  • 不同的运用程序可能会依靠同一个第三方类库的不同版别,不能要求同一个类库在同一个服务器只要一份,因而要确保每个运用程序的类库都是独立的,确保彼此隔离。
  • 布置在同一个web容器中相同的类库相同的版别能够同享
  • web容器也有自己依靠的类库,不能与运用程序的类库混淆。
  • web容器要支撑jsp的修改,需求支撑 jsp 修改后不用重启。

源码剖析JVM类加载机制

3.7 自定义类加载器

在介绍类加载器品种的时分,一共有四种,前面所说的都是前三品种加载器的一些机制,那假如咱们想自己自定义个类加载器要怎么完结呢?

自定义类加载器,只需承继ClassLoader抽象类,并重写findClass办法(假如要打破双亲派遣模型,需求重写loadClass办法)。下面是个自定义类加载器的比如:

public class ClassLoaderDrill {
    static class MyClassLoader extends ClassLoader {
        private String classPath;
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass将一个字节数组转为Class目标,这个字节数组是class文件读取后终究的字节 数组。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }
    public static void main(String args[]) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其间会把自定义类加载器的父加载器设置为运用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //创立 /com/xxx/xxx 的几级目录,跟你要加载类的目录一致
        Class clazz = classLoader.loadClass("com.test.jvm.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

留意:一个ClassLoader创立时假如没有指定parent,那么它的parent默许便是AppClassLoader。这个在ClassLoader的结构办法完结里能够看到。

4 类加载的进程

上述咱们介绍了类加载器及相关机制和完结源码,可是类加载器获取所需求的类这个动作,只是类加载全进程中的一部分。类从被加载到虚拟机内存中开端,到卸载出内存停止,它的整个生命周期包括:加载、验证、预备、解析、初始化、运用和卸载7个阶段。其间验证、预备、解析三个部分统称为链接,这7个阶段的产生顺序如图:

源码剖析JVM类加载机制

下面也给咱们简略介绍下每个阶段所履行的详细动作

4.1 加载

JVM 在该阶段的首要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节省加载到内存(JVM)中,并生成一个代表该类的 java.lang.Class 目标。该阶段JVM完结3件事:

  • 经过类的全限定名获取该类的二进制字节省(需求特别阐明的是咱们上述所说的类加载器相关动作,便是类加载进程中的这个阶段)
  • 将字节省所代表的静态存储结构转化为办法区的运转时数据结构
  • 在内存中生成一个该类的java.lang.Class目标,作为该类在办法区的各种数据的访问入口

4.2 验证

首要确保加载进来的字节省契合JVM标准。JVM 会在该阶段对二进制字节省进行校验,只要契合 JVM 字节码标准的才能被 JVM 正确履行,该阶段是确保 JVM 安全的重要屏障。

验证阶段会完结以下4个阶段的查验动作:

  • 文件格局验证:根据字节省验证
  • 元数据验证(是否契合Java语言标准):根据办法区的存储结构验证
  • 字节码验证(确定程序语义合法,契合逻辑):根据办法区的存储结构验证
  • 符号引证验证(确保下一步的解析能正常履行):根据办法区的存储结构验证

4.3 预备

该步首要为静态变量在办法区分配内存,并设置默许初始值。JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化。

4.4 解析

虚拟机将常量池内的符号引证替换为直接引证的进程,即将常量池中的符号引证转化为直接引证。

4.5 初始化

在预备阶段,类变量现已被赋过默许初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是履行类结构器办法的进程。

4.6 运用

运用阶段包括自动引证和被动引证,自动饮用会引起类的初始化,而被动引证不会引起类的初始化。当运用阶段完结之后,java类就进入了卸载阶段。

4.7 卸载

关于类的卸载,在类运用完之后,假如满意下面的状况,jvm就会在办法区垃圾收回的时分对类进行卸载。类的卸载进程其实便是在办法区中清空类信息,java类的整个生命周期就完毕了。

  • 该类一切的实例都现已被收回,也便是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader现已被收回。
  • 该类对应的java.lang.Class目标没有任何地方被引证,无法在任何地方经过反射访问该类的办法。

5 总结

最终介绍了下类加载的整个进程及履行的详细动作,其实每个节点去深挖也是有很多内容的,感兴趣的小伙伴能够再去深入了解。

本文正在参与「金石计划」