性能调优 9. 从JDK源码级别彻底剖析JVM类加载机制

1. 类加载运行全过程


‌‌‌  当用Java命令运行某个类的Main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

‌‌‌  通过Java命令运行代码流程和运行过程中加载类的过程,来了解类加载运行全过程。


‌‌‌ 通过Java命令执行代码的流程


‌‌‌  创建演示的类,运行其Main方法。


‌‌‌  package com.tuling.jvm;

‌‌‌  public class Math {
‌‌‌  public static final int initData = 666;
‌‌‌  public static User user = new User();

	public int compute() { //一个方法对应一块栈帧内存区域
	int a = 1;
	‌‌‌ int b = 2;
	‌‌‌ int c = (a + b) * 10;
	‌‌‌ return c;
	‌‌‌ }
	
	‌‌‌public static void main(String[] args) {
	‌‌‌Math math = new Math();
	‌‌‌math.compute();
	‌‌‌}
‌‌‌  }

‌‌‌  以Windows为例,LInux下差异不大。

‌‌‌  1. Window下java.exe调用jvm.dll创建Java虚拟机(C++实现)。

‌‌‌  java.exe由C++实现,jvm.dll是由C++实现的库函数(可以理解为jar包)。

‌‌‌  2. JVM初始化过程中,创建引导类加载器实例(C++实现)该类加载器加载sun.misc.Launcher类和加载启动器辅助类sun.launcher.LachuerHelp类。加载Launche类时候会静态初始化个Launcher实例,Launcher构造方法内部,创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

‌‌‌  3. 虚拟机调用C++代码通过JNI技术调用LaucherHelp.checkAndLoadMain()方法,会获取系统类加载器默认是AppClassLoader(应用类加载器,系统类加载器如果没有启动时候自定义,默认则使用AppClassLoader),*加载要运行的类这边是Math类

‌‌‌  4. Math类加载完成,JVM调用C++通过JNI技术,调用静态方法main()方法启动Java程序

‌‌‌  流程图参考

‌‌‌  第一种参考

在这里插入图片描述

‌‌‌  第二种参考

在这里插入图片描述
  
  注意

‌‌‌  1. 如果IDEA下Laucher构造函数中打断点不生效,可能是为 Java 提供 Debug 支持的类加载和 Launcher 的类加载都是由 Bootstrap 类加载器负责的,只是后者先发生,所以 Debug 功能实现的时候,Launcher 的构造器早已运行结束了。


类加载的过程


‌‌‌  流程图

在这里插入图片描述

‌‌‌  加载Class文件到JVM是过程有如下几步:

‌‌‌  加载->验证->准备->解析->初始化->使用->卸载

‌‌‌  1. 加载:在硬盘上查找并通过IO读入字节码文件(Class文件),使用到类时才会加载。例如调用类的main()方法,new对象时候等等,在加载阶段会在内存(JVM堆)中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口

‌‌‌  2. 验证:校验字节码文件的正确性。

‌‌‌  3. 准备:给类的静态变量分配内存,并赋予默认值。

‌‌‌  4. 解析:为了提升效率,将符号引用替换为直接引用,该阶段会把一些静态方法的符号引用,比如main()这个符号,替换为指向数据所存内存的指针或句柄等(了解下直接指针访问或者句柄访问数据),即直接引用(这些方法符号的代码在方法区地址,类加载后类相关信息会放在方法区)。这是所谓的静态链接过程(类加载期间完成),静态链接会在类加载期间存储在类对应的运行时常量池(有异议)

‌‌‌  动态链接(后面文章会讲)是在程序运行期间完成的将符号引用替换为直接引用,然后将直接引用放到,每个方法运行时候创建栈帧的动态链接中,比如在在运行阶段调用到方法时候,如下面代码。

‌‌‌  例如

‌‌‌  创建Math的实例math,然后调用math.compute()就会将math.compute()这个符号引用转成直接引用。

‌‌‌  package com.tuling.jvm;

‌‌‌  public class Math {
‌‌‌  public static final int initData = 666;
‌‌‌  public static User user = new User();

	public int compute() { //一个方法对应一块栈帧内存区域
	int a = 1;
	‌‌‌ int b = 2;
	‌‌‌ int c = (a + b) * 10;
	‌‌‌ return c;
	‌‌‌ }
	
	‌‌‌public static void main(String[] args) {
	‌‌‌Math math = new Math();
	‌‌‌math.compute();
	‌‌‌}
‌‌‌  }



‌‌‌  5. 初始化:对类的静态变量初始化为指定的值,执行静态代码块

‌‌‌  类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息

‌‌‌  类加载器的引用:这个类到类加载器实例的引用。

‌‌‌  对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class类型的对象实例放到堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点。

‌‌‌  注意

‌‌‌  1. 主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

‌‌‌  测试代码:


‌‌‌  public class TestDynamicLoad {

    static {
        System.out.println("*************load TestDynamicLoad************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*************load test************");
        B b = null;  //B不会加载,除非这里执行 new B()
    }
‌‌‌  }

‌‌‌  class A {
    static {
        System.out.println("*************load A************");
    }

    public A() {
        System.out.println("*************initial A************");
    }
‌‌‌  }

‌‌‌  class B {
    static {
        System.out.println("*************load B************");
    }

    public B() {
        System.out.println("*************initial B************");
    }
‌‌‌  }

‌‌‌  运行结果:
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************

‌‌‌  2. 访问类中的static final 成员时,JVM会在编译阶段对类执行编译优化,当类中有static final修饰的基本数据类型和字符串类型时,就会在编译阶段执行初始化,不会去加载类初始化,也就是使用时候不会加载类。


类的静态加载和动态加载


‌‌‌  静态加载:编译时候就确定要加载的类,不会去加载只是确认。比如new 对象等操作,确定加载的类不存在会报错。

‌‌‌  非静态内部类不能静态加载。

‌‌‌  动态加载:比如Java里头Class.ForName方式加载类。在程序运行时候,动态的加载类,加载不到可以跳过,编译时候不用确定加载的资源是否存在。
‌‌‌  动态加载可以选择延迟触发加载类的初始化过程,比如Class.ForName()可以设置参数,不在加载类时候初始化类。


2. 类加载器


‌‌‌  类加载器,就是加载Class文件到JVM的组件,它负责读取Class文件,并转换成 java.lang.Class 类的一个实例,使字节码.class 文件得以运行。

‌‌‌  另外,它还可以加载资源,包括图像文件和配置文件等


Java主要的类加载器


‌‌‌  Java中主要有 3 个类加载器,另外也可以自定义类加载器。

‌‌‌  1. 启动类加载器(Bootstrap ClassLoader)或者引导类加载器

‌‌‌  其实它属于 JVM 整体的一部分。

‌‌‌  创建:JVM启动时候创建。

‌‌‌  启动:JVM启动时候会调用启动类加载器。

‌‌‌  实现:原生代码(C/C++)实现,并不是继承 java.lang.ClassLoader,启动类加载器无法被 Java 程序直接使用。

‌‌‌  父类加载器:没有父类加载器,它是所有其它类加载器的最终父加载器

‌‌‌  加载的类:java 核心库,把一些核心的 Java 类加载进 JVM 中,负责加载 <JAVA_HOME>/jre/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
‌‌‌  JVM 一启动就将这些指定的类加载到内存中,避免以后过多的 I/O 操作,提高系统的运行效率。

‌‌‌  2. 扩展类加载器(Extension ClassLoader)

‌‌‌  创建:JVM启动时候借助C++调用启动类加载器,加载sun.misc.Launcher类时候会静态初始化创建Launcher实例,在其构造方法内部创建。

‌‌‌  实现:Java语言实现。

‌‌‌  被加载:由启动类加载器加载。

‌‌‌  父类加载器:它的父类加载器是启动类加载器,调用扩展类加载器的 getParent()方法获取父加载器会得到 null。

‌‌‌  继承的父类:java.net.URLClassLoader。

‌‌‌  加载类:加载的类为 Java 的扩展库,加载 <JAVA_HOME>/jre/lib/ext目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。

‌‌‌  加载类逻辑

‌‌‌  1. Extension ClassLoader加载器继承java.net.URLClassLoader而java.net.URLClassLoader又继承java.lang.ClassLoader。重写ClassLoader的loadClass()方法用来加载类,其中核心逻辑会调用ClassLoader的loadClass()方法来加载类,该方法加载类逻辑实现了双亲委派机制,所以该加载器加载类实现了双亲委派。

‌‌‌  2. ClassLoader的loadClass()方法会调用findClass()方法来读取类转成字节,默认是空方法。Extension ClassLoader则调用是URLClassLoader的findClass()方法来实现,只要传参,指定路径的字符串给findClass()方法,就会读取指定路径资源下的类转成字节返回。

‌‌‌  3. 应用程序类加载器(Application ClassLoader)

‌‌‌  创建:JVM启动时候借助C++调用启动类加载器,加载sun.misc.Launcher类时候会静态初始化创建Launcher实例,在其构造方法内部创建。

‌‌‌  实现:Java语言实现。

‌‌‌  被加载:由启动类加载器加载。

‌‌‌  父类加载器:它的父类加载器是扩展类加载器。

‌‌‌  继承的父类:java.net.URLClassLoader。

‌‌‌  加载类:JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH变量所指定的JAR包和类路径。加载类时候根据这些classpath按顺序查找。

‌‌‌  加载类逻辑

‌‌‌  1. Application ClassLoader加载器继承java.net.URLClassLoader而java.net.URLClassLoader又继承java.lang.ClassLoader。重写ClassLoader的loadClass()方法用来加载类,其中核心逻辑会调用ClassLoader的loadClass()方法来加载类,该方法加载类逻辑实现了双亲委派机制,所以该加载器加载类实现了双亲委派。

‌‌‌  2. ClassLoader的loadClass()方法会调用findClass()方法来读取类转成字节,默认是空方法。Application ClassLoader则调用是URLClassLoader的findClass()方法来实现,只要传参,指定路径的字符串给findClass()方法,就会读取指定路径资源下的类转成字节返回。

‌‌‌  注意

‌‌‌  1. 运行java进程时候,例如可根据-D java.system.class.loader=com.liu.lei_jia_zai.zi_ding_yi_jia_zai_qi.MySysTemClassLoader指定系统类加载器。不指定下默认就是AppClassLoader,则可通过 ClassLoader.getSystemClassLoader()获取到AppClassLoader

‌‌‌  2. 前面说过启动自定义类的Main方法时候,正常没有指定下加载器下,加载自定义类的类加载器使用的是系统类加载器。系统类加载器默认是AppClassLoader,即加载自定义类使用的是AppClassLoader。

‌‌‌  4. 自定义加载器

‌‌‌  负责加载用户自定义路径下的类包。

‌‌‌  父类加载器:因为继承 java.lang.ClassLoader,创建实例时候会先创建ClassLoader,在构造方法中设置父类加载器为系统类加载器,系统类加载器默认是AppClassLoader。

‌‌‌  继承的父类:java.lang.ClassLoader。

‌‌‌  被加载:由系统类加载器加载默认即系统类加载器默认就是AppClassLoader。


示例


‌‌‌  public class Test1 {

    public static void main(String[] args) {

        // 查看加载类的类加载器
        System.out.println("--------------------------------查看Java主要的加载器加载类的类加载器,验证是不是这些加载器加载START--------------------------------");
        // 查看加载核心类的类加载器
        System.out.println(String.class.getClassLoader());
        // 查看加载扩展类的类加载器
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        // 查看加载classpath下的类
        System.out.println(Test1.class.getClassLoader().getClass().getName());
        System.out.println("--------------------------------查看Java主要的加载器加载类的类加载器,验证是不是这些加载器加载END--------------------------------");

        System.out.println("--------------------------------查看Java主要的加载器的父类加载器START--------------------------------");
        // 获取系统类的加载器这边是AppClassLoader
        ClassLoader appClassLoader=ClassLoader.getSystemClassLoader();
        // 获取系统类的父类加载器
        ClassLoader extClassloader=appClassLoader.getParent();
        // 返回当前加载器的父类加载器
        ClassLoader bootstrapLoader=extClassloader.getParent();
        System.out.println("thebootstrapLoader:"+bootstrapLoader);
        System.out.println("theextClassloader:"+extClassloader);
        System.out.println("theappClassLoader:"+appClassLoader);
        System.out.println("--------------------------------查看Java主要的加载器的父类加载器END--------------------------------");



        System.out.println("--------------------------------查看Java主要的加载器加载的文件START--------------------------------");

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

        System.out.println("--------------------------------查看Java主要的加载器加载的文件END--------------------------------");

    }
‌‌‌  }

‌‌‌  注意

‌‌‌  1. 会发现AppClassLoader加载路径包含核心jar包路径,但是双亲委派原则这些核心jar包的类不会被AppClassLoader加载。


类加载器的双亲委派机制


‌‌‌  JVM类加载器是有亲子层级结构的,如下图

在这里插入图片描述


‌‌‌  双亲委派:加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。

‌‌‌  这种模型要求,除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器。假如有一个类要加载进来,一个类加载器并不会马上尝试自己将其加载,而是委派给父类加载器,父类加载器收到后又尝试委派给其父类加载器,以此类推,直到委派给启动类加载器,这样一层一层往上委派。只有当父类加载器反馈自己没法完成这个加载时,子加载器才会尝试自己加载。通过这个机制,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,同时这个机制也保证了安全性。设想如果应用程序类加载器想要加载一个有破坏性的 java.lang.System 类,双亲委派模型会一层层向上委派,最终委派给启动类加载器,而启动类加载器检查到缓存中已经有了这个类,并不会再加载这个有破坏性的 System 类。

‌‌‌  双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载


‌‌‌ 例子

‌‌‌  来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码核心逻辑在其loadClass方法。

‌‌‌  首先要知道AppClassLoader继承java.net.URLClassLoader而java.net.URLClassLoader又继承java.lang.ClassLoader。

‌‌‌  AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

‌‌‌  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

‌‌‌  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载,即调用parent.loadClass(name, false);或者是调用bootstrap类加载器来加载。

‌‌‌  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

‌‌‌  4. findClass方法调用的就是URLClassLoader的findClass方法,指定加载资源的字符串传参给findClass方法就行。



‌‌‌  //ClassLoader的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 {  //如果当前加载器父加载器为空则委托引导类加载器加载该类
                    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) {  //不会执行
            resolveClass(c);
        }
        return c;
    }
‌‌‌  }


‌‌‌ 设计双亲委派机制原因


‌‌‌  1. 沙箱安全机制

‌‌‌  有了双亲委派机制,自己写的java.lang.String.class类不会被实现双亲委派的类加载,父类加载器会先加载这些核心,这样便可以防止核心API库被随意篡改(比如不被自己写的类替代)。

‌‌‌  还有实现双亲委派的类加载器会继承ClassLoader,会使用其defineClass方法将加载核心类字的节数组转成类对象时候,安全校验机制也会禁止其转换成功。

‌‌‌  2. 避免类的重复加载

‌‌‌  当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

‌‌‌  看下面例子:

‌‌‌  双亲委派导致。自定义同名核心类下,运行其实是已加载的核心类的Main方法,所以报错。

‌‌‌  package java.lang;

‌‌‌  public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
‌‌‌  }

‌‌‌  运行结果:
‌‌‌  错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
‌‌‌  否则 JavaFX 应用程序类必须扩展javafx.application.Application



类加载器的全盘负责委托机制


‌‌‌  “全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入


‌‌‌ 自定义类加载器


‌‌‌  自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法。一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass用来加载类,默认实现是空方法,自定义类加载器主要重写findClass方法就行




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


‌‌‌  public class MyClassLoaderTest1 extends ClassLoader{
    private String classPath;

    public MyClassLoaderTest1(String classPath) {
        this.classPath = classPath;
    }

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

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

    public static void main(String args[]) throws Exception {
        // 指定加载资源目录
        MyClassLoaderTest1 classLoader = new MyClassLoaderTest1("G:\\myjar");
        //G:\myjar下创建com文件---liu文件---将User.class放进去
        Class clazz = classLoader.loadClass("com.liu.User");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("test", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
‌‌‌  }


打破双亲委派机制


‌‌‌  自定义加载器打破双亲委派机制,就是重写继承的ClassLoader的loadClass方法,这样就不会调用其双亲委派的逻辑



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

‌‌‌  public class MyClassLoaderTest2 extends ClassLoader{
    private String classPath;

    public MyClassLoaderTest2(String classPath) {
        this.classPath = classPath;
    }

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

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

    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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) {
                // 1. 先用借助系统类加载Object等核心类,借助双亲委派给最高层级的加载器可以加载到
                // 2. 注意系统类加载器加载类路径不要包含自定义类加载器的路径,不然冲突要加载类会被系统类加载器加载
                // 3. 加个验证跳过不是加载需要的类时候
                if(!name.startsWith("com.liu")){
                    try {
                        c = getParent().loadClass(name);
                    } catch (Exception e) {

                    }
                }


                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    // 按自定义类加载器加载
                    c = findClass(name);
                }

            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


    public static void main(String args[]) throws Exception {
        // 指定加载资源目录
        MyClassLoaderTest2 classLoader = new MyClassLoaderTest2("G:\\myjar");
        //G:\myjar下创建com文件---liu文件---将User.class放进去
        Class clazz = classLoader.loadClass("com.liu.User");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("test", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
‌‌‌  }



Tomcat打破双亲委派机制


‌‌‌Tomcat使用默认的双亲委派类加载机制可行性


‌‌‌  Tomcat是个web容器。

‌‌‌  分析1: 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

‌‌‌  使用默认的类加载器机制,是无法加载两个相同类库的不同版本的,默认的类加器是不管版本的,只在乎你的全限定类名,并且只有一份。在JVM 中,一个类由全限定类名和一个类加载器的实例 ID 作为唯一标识

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

‌‌‌  默认的类加载器是能够实现的,因为它的职责就是保证唯一性。

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

‌‌‌  该问题同第一个分析。

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

‌‌‌  jsp文件的热加载,jsp文件其实也就是class文件,如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。所以每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

‌‌‌  思路总结

‌‌‌  1. 每个应用创建单独的自定义类加载器(这边就是应用加载器),加载应用自己的类库,达到应用类库互相隔离。

‌‌‌  2. 创建一个公用的自定义加载器,只负责加载共享的类库,达到隔离。设置为所有应用加载器的父加载器,应用加载器在加载不是自己应用目录下的类库时候,委派给其加载,该加载器只会加载共享的类库。

‌‌‌  3. 创建Tomcat容器私有的类加载器,启动过程中,只加载web容器自己依赖的类库。

‌‌‌  4. 为每个JSP文件创建自定义加载器加载。JSP文件发生修改,就卸载JSP对应的自定义类加载器,重新创建自定义类加载器,在需要使用JSP对应的Class时候重新加载。


Tomcat自定义加载器详解


在这里插入图片描述

‌‌‌  这些箭头指向表示加载器的父加载器,不是继承类,注意下区分

‌‌‌  Tomcat的几个主要类加载器

‌‌‌  1. CommonLoader:Tomcat最基本的类加载器,加载路径中的Class可以被Tomcat容器本身以及各个Webapp访问。

‌‌‌  2. CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的Class对于Webapp不可见。

‌‌‌  3. SharedLoader:各个Webapp共享的类加载器,加载路径中的Class对于所有Webapp可见,但是对于Tomcat容器不可见。

‌‌‌  4. WebappClassLoader:各个Webapp私有的类加载器,加载路径中的Class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的Spring版本,这样实现就能加载各自的Spring版本。

‌‌‌  从图中的委派关系中可以看出

‌‌‌  1. CatalinaClassLoader和SharedClassLoader的父加载器是CommonClassLoader,可以使用CommonClassLoader加载的类,从而实现了公有类库的共用。

‌‌‌  2. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

‌‌‌  3. WebAppClassLoader的父加载器是SharedClassLoader,可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

‌‌‌  4. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件。它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的热加载功能

‌‌‌  Tomcat为了实现隔离性,没有遵守双亲委派,每个WebappClassLoader加载自己的目录下的Class文件,不会传递给父类加载器,打破了双亲委派机制。


Tomcat 如何打破双亲委派机制


‌‌‌  Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委派机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。


loadClass 方法


‌‌‌  先看Tomcat 类加载器的 loadClass 方法的实现,加载类逻辑从这里触发。


‌‌‌  public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        //1. 先在本地 cache 查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 从系统类加载器的 cache 中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
 
        // 3. 尝试用 ExtClassLoader 类加载器类加载,为什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 尝试在本地目录搜索 class 并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }
    
    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
‌‌‌  }

‌‌‌  loadClass 方法主要有六个步骤:

‌‌‌  1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。

‌‌‌  2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。

‌‌‌  3. 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委派机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。

‌‌‌  4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。

‌‌‌  5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。还有就是前面先用Web自定义加载器去加载自己应用的类,打破了双亲委派。

‌‌‌  6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。

‌‌‌  从上面的过程可以看到,Tomcat 的类加载器打破了双亲委派机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。不先用系统类加载器 AppClassLoader 去加载,是因为那就变成双亲委派机制了,这就是 Tomcat 类加载器的巧妙之处。


findClass方法


‌‌‌  再看 findClass 方法的实现。


‌‌‌  public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    Class<?> clazz = null;
    try {
            //1. 先在 Web 应用目录下查找类,
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
    }
    
    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
    }
    
    //3. 如果父类也没找到,抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }
    return clazz;
‌‌‌  }

‌‌‌  在 findClass 方法里,主要有三个步骤:

‌‌‌  1. 先在 Web 应用本地目录下查找要加载的类(找每个应用下的WEB-INF的classes和lib的类)。

‌‌‌  2. 如果没有找到,交给父加载器去查找,会调用到SharedClassLoader去加载。

‌‌‌  3. 如果父加载器也没找到这个类,抛出 ClassNotFound 异常。


模拟实现Tomcat的WebappClassLoader


‌‌‌  模拟实现Tomcat的WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离。




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


‌‌‌  public class MyClassLoader extends ClassLoader{
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        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;

        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();

                    // 非自定义的类还是走双亲委派加载,不是自定义类的前缀的的类就给父类加载器加载,参数Tomcat的自定义加载器详解图设置父加载器。
                    // 主要考虑每个类继承子Object等核心类,需要借助启动类加载器加载等。
                    if (!name.startsWith("com.liu.jvm")){
                        c = this.getParent().loadClass(name);
                    }else{
                        c = findClass(name);
                    }

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }

    public static void main(String args[]) throws Exception {
        	// 加载指定路径下的资源,这里看成一个应用对应的类加载器
        MyClassLoader classLoader = new MyClassLoader("G:\\all-project-demo\\target\\classes");
            // 加载指定路径下的资源的类
        Class clazz = classLoader.loadClass("com.liu.lei_jia_zai.tomcat.mo_ni_webappclassloader.User");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("test", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();
            // 再创建一个应用的类加载器
        MyClassLoader classLoader2 = new MyClassLoader("G:\\all-project-demo\\target\\classes");
        clazz = classLoader2.loadClass("com.liu.lei_jia_zai.tomcat.mo_ni_webappclassloader.User");
        obj = clazz.newInstance();
        method= clazz.getDeclaredMethod("test", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());
    }
‌‌‌  }

‌‌‌  运行结果:

	可以看出创建的两个类加载器实例不一样,说明每个应用使用到类加载器实例是不一样

‌‌‌  com.tuling.jvm.MyClassLoaderTest$MyClassLoader@266474c2

‌‌‌  com.tuling.jvm.MyClassLoaderTest$MyClassLoader@66d3c617

‌‌‌  注意

‌‌‌  1. 同一个JVM内,两个相同包名和类名的类对象可以共存,因为它们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要它们的类加载器也是同一个才能认为他们是同一个。

Tomcat的JasperLoader热加载原理

‌‌‌  原理

‌‌‌  后台启动线程监听jsp文件变化(比如监听文件修改时间),如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类(重新编译下),之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁。

  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在进行 JDK 1.8 的 JVM 参数调优时,可以考虑以下几个方面: 1. 堆内存设置: - 调整初始堆大小和最大堆大小,使用 `-Xms` 和 `-Xmx` 参数来设置。根据应用的负载情况和服务器的可用内存,合理分配堆内存大小。 2. 垃圾回收器选择: - JDK 1.8 默认使用的是并行垃圾回收器(Parallel GC)。如果应用有较高的并发需求,可以考虑使用并发标记清除垃圾回收器(CMS GC)或 G1 垃圾回收器(G1 GC)。 3. 并行度设置: - 根据服务器的 CPU 核心数量和应用负载情况,调整并行垃圾回收的线程数。使用 `-XX:ParallelGCThreads` 参数来设置,并行垃圾回收线程的数量。 4. 元空间(Metaspace)设置: - 元空间是 JDK 1.8 中替代永久代的内存区域。可以使用 `-XX:MaxMetaspaceSize` 参数来设置元空间的最大大小。 5. 垃圾回收相关参数: - 根据应用的特点和性能需求,调整垃圾回收相关参数。例如,可以使用 `-XX:MaxGCPauseMillis` 来设置最大垃圾回收停顿时间,以平衡吞吐量和停顿时间。 6. 监控与调优工具: - 使用 JDK 自带的工具,如 jstat、jmap、jstack 等,来监控应用的内存、垃圾回收情况和线程状态。根据监控结果,进行针对性的调优。 注意,JVM 参数的调优需要根据具体应用的特点和实际情况进行实验和测试,以获得最佳性能和稳定性。建议在进行参数调优前,先了解应用的负载情况和性能瓶颈,并备份原有的参数配置,以便在调优过程中出现问题时可以回滚。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值