性能优化|全面剖析类加载机制

743 阅读14分钟

本地代码运行过程:

  • 首先通过javac 命令将java文件编译成class文件
  • 然后虚拟机加载class文件到内存中(可以来源于网络)
  • 虚拟机验证class文件是否合法
  • 给类的静态变量分配内存,并进行零值处理
    • 基本类型(整型:0,布尔类型:false..)
    • 引用类型:设置为空
  • 解析符号引用
    • 将符号引用解析成直接引用,直接引用指的是具体内存地址或者句柄,这里是静态链接过程,如果在运行期间将符号引用解析为直接引用,则称为动态引用。
  • 初始化
    • 开始执行代码中的初始化语句,包括静态代码块,和给静态变量的赋值操作 在这里插入图片描述

后面就是使用和卸载的过程。

JVM中有哪几种类加载器

类加载器就是将class文件加载到jvm中。

  • 引导类加载器(Bootstrap Classloader):C语言编写,负责加载jre环境下的lib目录下所有class文件
  • 扩展类加载器(Extension Classloader):负债加载jre\lib\ext*写所有的class文件
  • 应用类加载器(Application Classloader):负载加载classpath下所有的class文件,也就是我们写的代码。
  • 自定义类加载器:按需加载自己需要加载的字节码文件

验证三种加载器加载的类文件:

    public static void main(String[] args) {
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }
        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
在这里插入图片描述
在这里插入图片描述

从图可以看出类加载对只会加载自己负责的那部分class文件到内存中。

类加载器初始化过程

我们通过看启动器(Launch)构造方法里面的内容,来一探究竟 类加载器是如何初始化的

    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);
        }}

在构造方法中,可以看出系统创建了两个加载器,分别为: ExtClassLoader和AppClassLoader,我们平时默认使用的类加载器就是使用

            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

这个加载器加载的,我们平时调用 Class.class.getClassLoader()方法返回的就是这个初始化的加载器

这三个类加载器有什么关系?

我们执行如下代码

 public static void main(String[] args) {
        System.out.println(TestJVMClassLoader.class.getClassLoader().toString());
        System.out.println(TestJVMClassLoader.class.getClassLoader().getParent().toString());
        System.out.println(TestJVMClassLoader.class.getClassLoader().getParent().getParent());
    }
在这里插入图片描述
在这里插入图片描述

可以发现加载我们创建代码的类加载器是AppClassLoader,AppClassLoader的父类是ExtClassLoader,ExtClassLoader的父类是null,这里出现了两个类加载器,还有一个引导类加载器呢,如果没有猜错的话,应该就是null,那么为什么会是null呢?
还记得我们前面说过引导类加载器是是用C语言写的,既然是C语言写的,又怎么能打印在这呢,所以我们现在画个图来梳理下这三者的关系: 在这里插入图片描述

双亲委派机制又是什么鬼?

有很多同学都听过这个名词,但是就是没有一篇文章能讲清楚到底什么是双亲委派机,今天我用我毕生所学,让同学们彻底理解什么是双亲委派机制。

什么是双亲委派机制?

我们直接看源码,就很容易理解什么是双亲委派;

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

我来解释下这段代码的执行过程:

  1. 使用findLoadedClass方法查找内存中的class对象,如果没有,则继续往下查找
  2. 如果parent不为空,则 指的是AppClassLoader的父类加载器ExtClassLoader,通过ExtClassLoader调用loadClass,加载class文件,
  3. 如果parent为空,则说明当前的加载器已经是ExtClassLoader了,这个时候就直接调用BootstrapClass去加载;
  4. 如果这个时候已经找到了class对象,那么就可以直接返回了,如果还是没有找到的话,就得调用子类加载器自己实现的findCLass方法。去加载我们自定义的class文件了。
  5. 如果子类加载器没有实现findClass方法,我们可以看到父类默认实现为:直接抛出了classNotFound异常,一般如果我们自定义类加载都只需要实现findClass方法。
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

我们用一张图来说下找“class的过程” 在这里插入图片描述

其实最后一步,自己想办法,也就是实现父类的findclass方法。 想必大家在看完这段讲解后,对双亲委派应该有个大致的了解了,如果真的认真看完这个流程的话,相信大家肯定会有疑问:
如果自己需要加载这个字节码的话,为什么不直接调用自己的findclass方法呢,还得一级一级往上找呢,JVM为什么要设置双亲委派机制呢?

为什么要设计双亲委派机制?

  • 沙箱安全机制:防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性 我们来验证下jvm双亲委派机制是否真的有效: 我们执行如下代码,同学们猜下执行结果是什么?
package java.util;
public class Date {
    public static void main(String[] args) {
        System.out.println("我被执行");
    }
}

执行结果: 在这里插入图片描述

为什么会出现这种情况呢,main方法为什么找不到呢? 其实这就是双亲委派机制在起作用,因为java系统中已经有同包名的Date类了,当我们运行我们的main方法是,他首先得要加载Date类。根据双亲委派机制,AppClassLoader得先询问父加载器有没有加载过这个Date,经过询问发现,父类已经加载了这个类,所以AppClass就不要自己再加载一遍了,直接使用父加载器加载的系统Date类,但是系统Date类是没有main方法的。所以才会出现上面的错误。

手动实现一个类加载器

package com.lezai;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class CustomClassloaderTest {
    static class CustomClassloader extends ClassLoader{
        private String classPath = "/Users/yangle/Desktop";

        /**
         * 自己实现查找字节码文件的逻辑,可以来自本地磁盘,也可以是来自网络
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        // 将文件读取到字节数组
        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) {
        CustomClassloader classLoader = new CustomClassloader();
        try {
            Class clazz = classLoader.loadClass("com.lezai.Test");
            Object obj = clazz.newInstance();
            Method method= clazz.getDeclaredMethod("out", String.class);
            method.invoke(obj,"乐哉");
            System.out.println(clazz.getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

实现步骤:

  • 自定义类继承ClassLoader类
  • 重写findClass方法,实现自己找字节码文件的逻辑
  • 如果不想遵守双亲委派机制,那么可以实现loadClass方法,不再去询问父类中是否加载过我们需要的字节码文件

如何打破双亲委派机制

我们如果需要打破双亲委派机制,只需要自己实现loadClass方法,不再去询问父类中是否加载过我们需要的字节码文件,然后直接调用findClass加载我们的类就行了。

tomcat为什么要打破双亲委派机制?

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

  • 我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

  • 再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行的。为什么?
  1. 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

  2. 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

  3. 第三个问题和第一个问题一样。

  4. 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

  • tomcat中主要的几个类加载器

    • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
    • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
    • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
    • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;
  • 几个类加载器之间的关系图 在这里插入图片描述

微信搜一搜【乐哉开讲】关注帅气的我,回复【干货领取】,将会有大量面试资料和架构师必看书籍等你挑选,包括java基础、java并发、微服务、中间件等更多资料等你来取哦。