源码剖析JVM类加载机制

1 前言

我们平常开发中,都会部署开发的项目或者本地运行main函数之类的来启动程序,那么我们项目中的类是如何被加载到JVM的,加载的机制和实现是什么样的,本文给大家简单介绍下。

2 类加载运行全过程

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM,通过Java命令执行代码的大体流程如下

源码剖析JVM类加载机制_第1张图片

从流程图中可以看到类加载的过程主要是通过类加载器来实现的,那么什么是类加载器呢?

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类加载机制_第2张图片

通过运行结果,我们会发现我自定义的当前运行类的类加载器是AppClassLoader,Logging这个类的类加载器是ExtClassLoader,而且类加载器之间是有父子关系关联的。但String的类加载器却为null,ExtClassLoader的父加载器也为null,是意味着String类不是通过类加载器加载的?那如果可以加载它又是怎么被加载的呢?为什么我们获取不到BootstrapClassLoader呢?后面我们会进行解读。

3.3 类加载器的机制

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

双亲委派机制

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它搜索的范围没有找到所需的类),子加载器才会尝试自己取加载。

双亲委派机制说简单点就是:对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。

源码剖析JVM类加载机制_第3张图片

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类加载机制_第4张图片

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类加载机制_第5张图片

下面也给大家简单介绍下每个阶段所执行的具体动作

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 总结

最后介绍了下类加载的整个过程及执行的具体动作,其实每个节点去深挖也是有很多内容的,感兴趣的小伙伴可以再去深入了解。

你可能感兴趣的:(jvm,java,开发语言,源代码管理)