深入了解JVM的类加载机制及双亲委派模型

84 阅读10分钟
  • 在写这篇文章之前,我看了一些其他人写的相关文章,不禁感叹这个行业从来不缺厉害之人,有些文章写的思路清晰又全面,真是学习的楷模。向他人学习,吸取他人的经验,也是实现自我提升的一种很好的方式。

类加载过程

  • .java文件在编译后会生成相应的.class文件,这些class文件中描述了类的各种信息,这些文件最终需要被加载到虚拟机中才能被使用。
  • 类加载的过程分为5个步骤:加载->验证->准备->解析->初始化,其中验证、准备、解析统称为连接过程。

image.png

加载

  1. 通过一个类的全限定名来获取该class文件的二进制字节流;
  2. 将字节流的静态存储结构转化为方法区的运行时数据结构;
  3. 为上面的class文件在堆中生成一个java.lang.class对象,作为该类的访问入口。

验证

  • 验证阶段主要是确保Class文件的字节流中包含的信息要符合虚拟机规范中的要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
  • 验证阶段主要包含四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
  1. 文件格式验证是验证class文件的字节流是否符合class文件格式规范,保证字节流的数据能够正确解析并存储到方法区的数据结构中,而且当前的虚拟机版本能够对其进行处理。文件格式验证通过后字节流的数据信息就被存储到方法区的运行时数据结构中了,后面的验证阶段都是对方法区中的数据进行的验证;
  2. 元数据验证是对字节码描述的信息进行语义分析,保证其描述的信息符合《Java语言规范》的要求。其实就是对类的元数据信息进行验证;
  3. 上一步对元数据进行验证后,字节码验证就是对方法体进行验证。通过数据流和控制流来分析程序的语义是否合法,符合逻辑。保证方法运行时不会做出错误或者危害虚拟机的行为。
  4. 符号引用验证保证解析阶段正常执行。

准备

  • 为类的静态变量分配内存,并初始化默认值(在方法区上分配,实例变量在对象实例化时在堆上分配)
  • 补充:对于普通静态变量来说,具体的赋值是在初始化阶段中进行的;而对于final修饰的静态变量来说,在该阶段就已经赋值了

解析

  • 将符号引用替换为直接引用的过程。主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
  • 某些方法、字段等在编译期就可以确定,所以解析阶段会替换运行时确定不会发生变动的那些符号引用,这就是所谓静态链接的过程。而动态链接是在程序运行期间完成符号引用替换为直接引用。

初始化

  • 编译器整合静态变量赋值操作和执行静态代码块的两个操作整合成一个方法叫做clinit(),初始化就是执行clinit方法的过程。

JVM的类加载器(ClassLoader)

类加载器介绍

classloader.png

  1. 启动类加载器/引导类加载器(BootStrap ClassLoader):BootStrap ClassLoader是用C++语言实现的,是JVM的一部分。加载支撑JVM运行的<JAVA_HOME>/lib目录下的核心类库(例如:rt.jar,charsets.jar)。该类加载器只为JVM提供加载服务,开发人员不能用它来加载自己的类;
  2. 扩展类加载器(Extension ClassLoader):加载支撑JVM运行的<JAVA_HOME>/lib/ext扩展目录下的Jar包。
  3. 应用程序类加载器/系统类加载器(Applicationn ClassLoader):加载classpath路径下的类库(主要是开发人员自己写的);
  4. 自定义类加载器(User Defined ClassLoader):一般情况下,上面三种类加载器可以完成正常的类加载,当需要加载其他自定义路径下的类时,需要自定义类机载器来完成。

类加载器的初始化

image.png

  • 上图为JVM的启动过程,其中涉及到类加载器的初始化过程。
  • 类加载器的初始化过程
  1. 创建JVM启动类实例sun.misc.Launcher,Laucner初始化使用了单例模式,保证JVM虚拟机内部只有一个sun.misc.Launcher实例;
  2. 在Launcher构造方法内部,创建了两个类加载器,分别是扩展类加载器和应用程序类加载器,其中,扩展类加载器的parent是null(因为BootStrap类加载器是C++实现的),应用程序类加载器的parent为扩展类加载器;
  3. JVM默认使用Launcher的getClassLoader()方法,返回AppClassLoader实例加载我们的应用程序;

双亲委派模型

  • JVM的类加载机制是按需加载的模式运行的,也就是代表着:所有类并不会在程序启动时全部加载,而是当需要用到某个类发现它未加载时,才会去触发加载的过程。
  • 如果一个类加载器收到类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类加载器(parent),每一层都是如此。只有当父类加载器反馈自己无法完成加载请求时,子类加载器才会尝试自己去加载,这就是双亲委派模型(类加载器之间并不存在相互继承或包含关系,从上至下仅存在父加载器的层级引用关系)

image.png

双亲委派模型源码解析

  • 应用程序类加载器、扩展类加载器都间接的继承了ClassLoader类,ClassLoader类是Java类加载机制的顶层设计类,它是一个抽象类。双亲委派模型就是在loadClass()方法实现的。
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 {
                    //如果父类加载器为null,则委托给BootStrap类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果未找到类,则抛出ClassNotFoundException
            }

            if (c == null) {
                //如果都没有找到,则通过自定义实现的findClass去加载
                long t1 = System.nanoTime();
                c = findClass(name);

                //这是记录类加载相关数据的(比如耗时、类加载数量等)
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        //是否需要在加载时进行解析,如果需要则触发解析操作
        if (resolve) {
            resolveClass(c);
        }
       //返回加载后生成的Class对象
        return c;
    }
}

上述方法的核心步骤有三步:

  1. 首先,检查制定名称的类是否已经加载过了,如果已经加载过了,就不需要再加载,直接返回,findLoadedClass();
  2. 如果此类没有被加载过,再判断一下是否有父类加载器,如果有,则调用父类加载器的LoadClass()方法加载,没有则调用BootStrap类加载器加载(此步骤为向上委托的过程);
  3. 如果父加载器及BootStrap类加载器都没有找到指定的类,则调用当前类加载器的findClass()来完成类加载。
  • ClassLoader类的其他几个核心方法:
//findClass()方法:这个方法是留给子类重写的
protected Class<?> findClass(String name) 
            throws ClassNotFoundException {
    // 直接抛出异常
    throw new ClassNotFoundException(name);
}

//defineClass()方法
protected final Class<?> defineClass(String name, byte[] b,
        int off, int len) throws ClassFormatError
{
    // 调用了defineClass方法
    // 将字节数组b的转换成字节码并返回一个Class对象,底层调用的本地native方法
    return defineClass(name, b, off, len, null);
}

// getParent()方法
@CallerSensitive
public final ClassLoader getParent() {
    // 如果当前类加载器的父类加载器为空,则直接返回null
    if (parent == null)
        return null;
    // 如果不为空则先获取安全管理器
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // 然后检查权限后返回当前classLoader的父类加载器
        checkClassLoaderPermission(parent,
                Reflection.getCallerClass());
    }
    return parent;
}

为什么要设计双亲委派模型

  1. 沙箱安全机制:自己写的java.lang.String类不会被加载,可以防止核心API类库被随意篡改。
  2. 避免一个类在不同层级的类加载器中重复加载:当父类已经加载了该类,子类没必要再加载一次,保证被加载类的唯一性。

自定义类加载器

  • 自定义加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法:一个loadClass()实现了双亲委派模型,一个findClass().自定义类加载器主要是重写findClass()方法
  • 自定义类加载器的父加载器默认为AppClassLoader,因为初始化自定义类加载器时会先初始化父类ClassLoader,它会把自定义加载器的父加载器设置为AppClassLoader.
public class ClassLoaderTest extends ClassLoader {
    //jar包或.class文件的存放路径
    private String classPath;

    public ClassLoaderTest(String classPath) {
        this.classPath = classPath;
    }
    //主要是调用defineClasss(),将代表类的字节数组转换为Class对象
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            //获取字节数组
            byte[] data = loadByte(name);
            //将一个字节数组转为Class对象。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    //从路径读取jar包或.class文件,并转换为字节数组返回
    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;
    }
}

Tomcat自定义类加载器

Tomcat打破双亲委派模型

  • 双亲委派机制可以打破吗?答案是肯定的,重写类加载的方法loadClass(),实现自己的加载逻辑,不委派给双亲去加载。
  • Tomcat是个容器,它主要解决什么问题?
  1. 一个web容器可能要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,保证不同应用程序的类库都是相互隔离的;
  2. 部署在同一个web容器中相同的类库相同的版本可以共享;
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆;
  4. 支持jsp修改后不用重启。
  • 上述的1和3如果不打破双亲委派模型,是无法实现的。

Tomcat自定义加载器

image.png

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容易本身以及各个WebApp访问;
  • CatalinaClassLoader:各个WebApp共享的,但对于WebApp不可见;
  • SharedClassLoader:各个WebApp共享的,但对于Tomcat容器不可见;
  • WebAppClassLoader:各个WebApp私有的类加载器,加载路径中的class仅对当前WebApp可见。

Tomcat如何打破双亲委派模型

  • Tomcat中只有WebAppClassLoader打破了双亲委派机制,该加载器重写了loadClass()方法。
  • loadClass()方法的核心步骤如下:
  1. 从当前ClassLoader的本地缓存中查找该类是否被加载过,如果找到则返回;
  2. 如果没有,则从系统类加载器的缓存中查找该类是否被jvm加载过;
  3. 如果没有,则使用ExtClassLoader类加载器加载,WebAppClassLoader并没有委托父类加载器去加载,而是使用ExtClassLoader类加载器加载,ExtClassLoader类加载器依旧遵循双亲委派,防止核心类库被篡改(此步骤AppClassLoader打破了双亲委派机制);
  4. 如果没有,则调用自己的findClass()方法来完成类加载,先在WEB-INF/classes中加载,再从WEB-INF/lib中加载;
  5. 如果仍未加载成功,则委派给自己的父类加载器去加载。