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

本地代码运行过程:

  • 首先通过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版本;
  • 几个类加载器之间的关系图

作者:乐哉

图片:来源于网络,如有侵权,联系删除。

本文分享自微信公众号 - 乐哉开讲(Lezaikaijiang),作者:乐哉开讲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 原来Redis里面的事务是这么操作的

    Redis事务不同于其他数据库的事务,它是一种弱事务,它的用法很简单,不需要记太多的事务隔离级别和事务的传播性,这就要求我们不能把redis事务当作一个正常的事...

    AI码师
  • 15分钟快速了解eureka及实战

    我们不管在进行分布式开发还是微服务开发,都需要接触一个组件,那就是服务治理中心,必须有一个组件为你提供和发现服务的功能,注册中心可以由zookeeper、rei...

    AI码师
  • 性能优化|MVCC通俗理解与事务隔离级别实战操作

    为每一行数据添加锁,加锁慢,容易出现死锁竞争,因为锁的每一行数据,锁的力度小,所以并发高,Innodb支持行级锁,行级锁是支持事务的。

    AI码师
  • Java类加载器

    通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类...

    用户5475193
  • 那些有趣的代码(二)--偏不听父母话的 Tomcat 类加载器

    看 Tomcat 的源码越看越有趣。Tomcat 的代码总有一种处处都有那么一点调皮的感觉。今天就聊一聊 Tomcat 的类加载机制。

    用户2060079
  • Java 类机制(2)---- 类加载过程

    大家好,在该专栏的上一篇文章中我们介绍了一下关于 Java 中类的相关知识点。那么这篇文章我们来看一下一个 Java 类是怎么被虚拟机加载并使用的,本文内容参考...

    指点
  • 《前端那些事》聊聊前端的按需加载

    树酱
  • Java 类加载器揭秘-gitchat

    类加载器作为 JVM 加载字节码到内存中的媒介,其重要性不言而喻,另外在职场面试时候也会被频繁的问道,了解类加载器的原理,能灵活的自定义类加载器去实现自己的功能...

    加多
  • JVM系列——java文件到JVM中的整个过程

    首先是编写一个HelloWorld.java类,然后通过这一系列的编译操作,最终成了HelloWorld.class文件。然后把HelloWorld.class...

    田维常
  • 浅入Java ClassLoader

    ClassLoader是用来加载Class 文件的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 .class...

    日薪月亿

扫码关注云+社区

领取腾讯云代金券