前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >性能优化|全面剖析类加载机制

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

作者头像
AI码师
发布2020-11-19 15:54:29
3550
发布2020-11-19 15:54:29
举报

本地代码运行过程:

  • 首先通过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文件,也就是我们写的代码。
  • 自定义类加载器:按需加载自己需要加载的字节码文件

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

代码语言:javascript
复制
    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)构造方法里面的内容,来一探究竟 类加载器是如何初始化的

代码语言:javascript
复制
    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,我们平时默认使用的类加载器就是使用

代码语言:javascript
复制
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

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

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

我们执行如下代码

代码语言:javascript
复制
 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语言写的,又怎么能打印在这呢,所以我们现在画个图来梳理下这三者的关系:

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

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

什么是双亲委派机制?

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

代码语言:javascript
复制
 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方法。
代码语言:javascript
复制
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

我们用一张图来说下找“class的过程”

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

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

  • 沙箱安全机制:防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性 我们来验证下jvm双亲委派机制是否真的有效:我们执行如下代码,同学们猜下执行结果是什么?
代码语言:javascript
复制
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方法的。所以才会出现上面的错误。

手动实现一个类加载器

代码语言:javascript
复制
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版本;
  • 几个类加载器之间的关系图

作者:乐哉

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 乐哉开讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本地代码运行过程:
  • JVM中有哪几种类加载器
    • 类加载器初始化过程
    • 这三个类加载器有什么关系?
    • 双亲委派机制又是什么鬼?
      • 什么是双亲委派机制?
        • 为什么要设计双亲委派机制?
          • 手动实现一个类加载器
            • 如何打破双亲委派机制
              • tomcat为什么要打破双亲委派机制?
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档