1. 从JDK源码级别彻底刨析JVM类加载机制

JVM性能调优

  • 1. 类加载的运行全过程
    • 1.1 加载
    • 1.2 验证
    • 1.3 准备
    • 1.4 解析
    • 1.5 初始化
  • 2. 类加载器和双亲委派机制
    • 2.1 类加载器的加载过程
    • 2.2 双亲委派机制
  • 3. 全盘负责委托机制
  • 4. 自定义类加载器示例
  • 5. 打破双亲委派机制
  • 6.Tomcat 如果使用默认的双亲委派类加载机制行不行?
  • 7. Tomcat打破双亲委派机制
  • 8. 自定义tomcat的war包类加载器

本文是按照自己的理解进行笔记总结,如有不正确的地方,还望大佬多多指点纠正,勿喷。

课程内容:

1、从java.exe开始讲透Java类加载运行全过程

2、从JDK源码级别剖析JVM核心类加载器

3、从JDK源码级别剖析类加载双亲委派机制

4、手写自定义类加载器打破双亲委派机制

5、Tomcat类加载机制深度剖析

6、手写Tomcat类加载器实现多版本代码共存隔离

1. 类加载的运行全过程

当我们用java命令运行某个类的main函数的启动程序时,首先需要通过类加载器把主类加载到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();
    }
}

通过Java命令执行代码的类加载全过程

1. 从JDK源码级别彻底刨析JVM类加载机制_第1张图片

其中loadClass的类加载过程有以下几步:

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

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等,在加载阶段会在内存区域生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 验证:校验字节码文件的准确性;
  • 准备:给类的静态变量分配内存,并赋予默认值;
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法替换为只想数据所在内存区域的指针或句柄(直接引用),这是所谓的静态链接过程,动态链接是在程序运行过程完成的将符号引用替换为直接引用;
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块;

1. 从JDK源码级别彻底刨析JVM类加载机制_第2张图片

1.1 加载

在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过类的全限定名来获取定义此类的二进制文件流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

《Java虚拟机规范》对这三点的要求并不是特别具体,所以留给虚拟机实现与Java应用的灵活度非常大,例如我们通过一个类的全限定类名来获取定义此类的二进制字节流这条规则,它并没有指明二进制文件流必须得从某个Class文件中获取,确切的说根本没有指明说要从哪里获取,如何获取。这也是日后的JAR、WAR、EAR格式的基础。

这一阶段就是先把字节码文件丢到内存。但是加载之前要先验证一下。

1.2 验证

验证是连接的第一步,这一个阶段的目的是保证Class文件的字节流中包含的信息符合的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致上会完成下面四个阶段的校验动作:

  1. 文件格式验证:这个阶段要验证字节流文件是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。例如:是否以魔数0xCAFEBABE开头,主次版本是否在当前Java虚拟机的接受范围之内等;
  2. 元数据验证:这个阶段主要是对字节码描述的信息进行语义分析,以确保其描述信息符合的要求,例如:这个类是否有父类,这个类是否继承了不允许继承的类(被final修饰的类)等;
  3. 字节码验证:这个阶段验证过程是最复杂的一个过程,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二个阶段对元数据信息中的数据类型校验分析完毕后,这阶段就要对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的行为。
  4. 符号引用验证:最后一个阶段校验行为发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作在链接阶段的第三阶段-解析阶段中发生。符号引用验证可以看作是对类自身以外的各类信息进行匹配校验,通俗来说就是,该类是否缺少或被禁止访问它依赖的某些类、方法、字段等资源。

1. 从JDK源码级别彻底刨析JVM类加载机制_第3张图片

1. 从JDK源码级别彻底刨析JVM类加载机制_第4张图片

1.3 准备

准备之前要先验证各种格式等是否正确。准备就是把静态变量初始值赋值。这个赋值与最终值没有关系,比如Boolean就会赋值false,int就先赋值为0这种。

1. 从JDK源码级别彻底刨析JVM类加载机制_第5张图片

1.4 解析

解析阶段是将Java虚拟机将常量池内的符号引用替换直接引用的过程。

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。
  • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能简介定位到目标的句柄。

在解析的时候会将静态方法,比如main方法就是静态方法,这些修饰符、方法、返回值、类名其实都是符号,这些符号加载到内存里面去,是有一个地址,这样的名称我们称为静态的符号,直接引用就是这些符号被加载到内存里面去,他有一个内存地址,有一个位置,那个位置就是代码的直接引用。这只是一个大致的过程,里面还是有很多细节的。

1. 从JDK源码级别彻底刨析JVM类加载机制_第6张图片

1.5 初始化

类的初始化是类加载过程中的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余的动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行执行类中编写的Java程序代码,将主导权移交给应用程序。

在准备阶段。变量已经进行过一次系统要求的初始零值,而在初始化阶段则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。可以简单的来说,初始化阶段就是执行类构造器方法的过程。需要注意的是()方法并不是程序员在应用程序中直接编写的方法,它是Java编译器自动生成的。下面是的介绍:

  • 它是由编译器自动收集类中的所有类变量赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是有语句在源文件中出现的顺序来决定的。

  • 它与类的构造函数不同,它不需要显示地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的()已经执行完毕。

  • 由于父类()方法先执行,也就意味着父类中定义的静态语句块要优先与子类的变量赋值操作。
    ()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法;

  • 接口中不能使用静态语句块,但任然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不用的是,执行接口的()方法不需要先执行父接口的()方法,因为只有父接口定义的变量被使用时,父接口才会被初始化

其实就是将类的静态变量初始化int、Boolean等默认数值赋值为正真的值

============================================================================================

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

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

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

注意, JVM里面的类加载其实应该算是懒加载。

主类在运行过程中如果使用到其它类,会逐步加载这些类。

jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

/**
 * 测试动态加载
 */
public class TestDynamicLoad {

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

    public static void main(String[] args) {
        A a = 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

当我们把B b = null 改为 B b = new B()时,运行结果:

load TestDynamicLoad
load A
initial A
load test
load B
initial B

进一步说明我们的静态代码块是要比我们实例化对象之前加载,而且只会随着类的加载而加载。

我们知道,类是由类加载器负责将class文件加载到我们的Jvm虚拟机中的,但是java中有哪几种类加载器呢?每个类加载分别会加载哪些类呢?接下来我们继续探讨一下java中的类加载器。

2. 类加载器和双亲委派机制

上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器;

  • 引导类加载器:负责加载支撑JVM运行的位于Jre的lib目录下的核心类库,比如 rt.jar、charsets.jar等。(这个不用管,c++会帮忙实现的。)

  • 扩展类加载器:负责加载支撑JVM运行的位于Jre的lib目录下的ext扩展目录中的Jar类包

  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载我们自己写的那些类

  • 自定义加载器:负责加载用户自定义路径下的类

上面三种是JVM自己帮我们生成的,第四个是自定义的。

看一个类加载器示例

/**
 * 测试Jdk类加载器
 */
public class TestJdkClassLoader {

    public static void main(String[] args) {

        /*String  位于jre的lib下*/
        System.out.println(String.class.getClassLoader());
        /*DESKeyFactory 位于jre的lib下的ext目录*/
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        /*classPath路径下*/
        System.out.println(TestJdkClassLoader.class.getClassLoader().getClass().getName());
        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the bootstrapLoader : " + bootstrapLoader);
        System.out.println("the extClassloader : " + extClassloader);
        System.out.println("the appClassLoader : " + appClassLoader);
        System.out.println();
        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"));

    }
}

我们现在只看前三个这个打印的结果,就是下面这三句话:

/*String  位于jre的lib下*/
System.out.println(String.class.getClassLoader());
/*DESKeyFactory 位于jre的lib下的ext目录*/
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
/*classPath路径下*/
 System.out.println(TestJdkClassLoader.class.getClassLoader().getClass().getName());

这三句话应该打印出什么结果呢?为什么是这个结果呢?

//打印结果
null      //注意: 1. bootstrapLoader是由c++语言实现的,所以会打印为null
sun.misc.Launcher$ExtClassLoader//这个就是位于jre的lib下的ext目录
sun.misc.Launcher$AppClassLoader//2.虽然应用程序类加载器打印了jre/lib下的核心类库,但是它其实只加载/target/class目录下的class类

2.1 类加载器的加载过程

参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。

sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例

在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

源码剖析:

​ 我们查看Launcher类,在声明静态变量的时候创建Launcher实例,在构造方法啊中创建扩展类加载器和应用类加载器

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    //Launcher的构造方法
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //构造扩展类加载器,在构造的过程中将其父加载器设置为null
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //创建应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
            //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
}

在Launcher.ExtClassLoader.getExtClassLoader()中创建扩展类加载器,这里会调用到顶层ClassLoader类的构造方法,只不过这里扩展类加载器传的parent为null

        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

在Launcher.AppClassLoader.getAppClassLoader(var1)中创建应用类加载器,这里会把父类加载器ExtClassLoader作为参数传入进来,注意,这里的两个类加载器不是类上的继承关系,只是AppClassLoader的parent属性指向了ExtClassLoader实例

        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }

父类构造方法

    public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }

父类的父类的构造方法

    protected SecureClassLoader(ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        initialized = true;
    }

父类的父类的父类的构造方法,为parent属性赋值

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

2.2 双亲委派机制

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

1. 从JDK源码级别彻底刨析JVM类加载机制_第7张图片

​ 先解释一下什么是双亲委派机制: 加载某个类时,如果自己加载过的类中没有找到,会先委托父加载器寻找目标类,如果还是找不到则继续再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。

​ 比如:我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里如果没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己尝试加载,在自己的类加载路径里也没找到对应的Math类,则又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器在自己的类加载路径里找Math类,找到后自己加载Math类。

思考一下,这里从下往上,再从上往下走了一圈,何不直接从引导类加载器开始加载呢?

那是因为我们很多的业务都是自己写的类实现的,这些类基本都是通过应用程序类加载器加载的,如果我们这些类多次使用,我们都要从引导类加载器来判断,那无疑对于我们的性能损耗会更大,而我们从应该程序类加载器开始加载,当加载我们自己写的类的时候,第一次走过一遍双亲委派机制后,还是由我们的应用程序类加载器进行加载,当下次再次使用这个类的时候,会先判断当前类加载器是否加载过这个类,如果加载过直接就返回了,这样性能损耗更低一点。

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

我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

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

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

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

  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) {//如果当前加载器父加载器不为空则委托父加载器加载该类
                        //parent属性不为空则调用父加载器加载类
                        c = parent.loadClass(name, false);
                    } else {//如果当前加载其父加载器为空则委托引导类加载器加载该类
                        //如果parent为空,则调用引导类加载器加载
                        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;
        }
    }

思考一下双亲委派机制的好处有哪些?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

这里做一个demo,我们自定义一个和String类相同路径相同名称的类,看看结果会怎么样

package java.lang;


public class String {

    public static void main(String[] args) {
        System.out.println("******自定义String的Main方法");
    }
}

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

这是因为双亲委派机制的存在,当我们要加载java.lang.String的时候,应用程序类加载会向上委托,
而我们的jre的lib下也有一个相同类路径的String类,此时会返回这个String类信息,
但是这个String类是没有main方法的,就会出现以上错误。
*/

3. 全盘负责委托机制

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

4. 自定义类加载器示例

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

首先写一个People类

public class People {
        private String name;

    public People() {
        // 空构造函数
    }


    public People(String name) {
            this.name = name;
        }

        public void printf() {
            System.out.println("Hello " + name);
        }

}

1. 从JDK源码级别彻底刨析JVM类加载机制_第8张图片

1. 从JDK源码级别彻底刨析JVM类加载机制_第9张图片

/**
 * 自定义类加载器测试
 */
public class MyClassLoaderTest {

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

        @Override
        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 ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {


        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");

        //D盘创建 test/ding 目录,将People.class丢入该目录
        Class clazz = classLoader.loadClass("ding.People");

        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("printf", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());

    }
}

运行结果:

自定义类加载器加载类成功

1. 从JDK源码级别彻底刨析JVM类加载机制_第10张图片

​ 对于结果打印的是类加载器是AppClassLoader,因为我们的项目中也有People这个类,而自定义类加载器的parent属性是应用程序类加载,这里是通过ClassLoader的构造方法实现的。

​ 当我们把People这个类从工程中删除掉重新执行代码, 但是前提自己要有一个类加载器呢。运行结果如下:

1. 从JDK源码级别彻底刨析JVM类加载机制_第11张图片

当我们自定义类加载器的时候如何打破双亲委派机制呢?

5. 打破双亲委派机制

意思就是我不委托上面的帮我加载了,我自己去加载。

​ 可以回想,我们双亲委派机制的实现机制代码是在ClassLoader的loadClass方法中,那我们可以重写这个方法来改变双亲委派的逻辑。用自定义类加载器加载我们自己实现的java.lang.String.class。

那我们怎么做呢,我们把这个双亲委派机制这块的代码重写,重写改变他的逻辑。写到MyClassLoaderTest里面去。

//ClassLoader的loadClass方法,里面实现了双亲委派机制
  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.
                    //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                    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;
        }
    }

现在这个时候MyClassLoaderTest类的全部源码如下:

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

public class MyClassLoaderTest {
    static 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;

         }
//

        @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();
                throw new ClassNotFoundException();
            }

        }

        //ClassLoader的loadClass方法,里面实现了双亲委派机制
        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();


                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                        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;
            }
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader("D:/test");

        //D盘创建 test/ding 目录,将People.class丢入该目录
        Class clazz = classLoader.loadClass("ding.People");
        //System.out.println(clazz);

        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("printf", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

现在我们对代码进行改造,我们把People类还原到项目中去,但是仍然让自定义类加载器加载我们D盘的People类。就是还原之前的People类,在idea中和D盘都要有。

现在开始运行,报了一个错误:

java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at MyClassLoaderTest$MyClassLoader.loadByte(MyClassLoaderTest.java:16)
	at MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:29)
	at MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:30)
	at MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at MyClassLoaderTest.main(MyClassLoaderTest.java:75)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:30)
	at MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at MyClassLoaderTest.main(MyClassLoaderTest.java:75)
Caused by: java.lang.ClassNotFoundException
	at MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:33)
	at MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 7 more

Process finished with exit code 1

这个错误是找不见这个Object.class这个文件。

这个问题的原因是,当打破了双亲委派机制后,CustomClassLoader自定义类加载器在查找并加载java.lang.Object类时,由于该类已经被Bootstrap ClassLoader加载,并且双亲委派机制被打破,所以该类被CustomClassLoader自定义类加载器重复加载,而导致了ClassNotFoundException异常.

解决这个问题可以考虑在自定义类加载器中添加其他逻辑,比如先尝试使用父类加载器来加载Java Lang包下的类,而避免直接从类加载路径中加载Java Lang包下的类。这个不能直接把Object.class复制出来到D盘是因为有沙箱机制,这个就是那个引导加载类里面的东西,因此我们只能想办法让引导类去加载这个class

1. 从JDK源码级别彻底刨析JVM类加载机制_第12张图片

源码:

//ClassLoader的loadClass方法,里面实现了双亲委派机制
        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();


                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                        long t1 = System.nanoTime();
                        if (!name.startsWith("ding")){
                            c = this.getParent().loadClass(name);
                        }else {
                            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;
            }
        }

6.Tomcat 如果使用默认的双亲委派类加载机制行不行?

以Tomcat类加载为例,Tomcat如果使用默认的双亲委派类加载机制行不行?我们思考一下: Tomcat是个web容器,那么它要解决什么问题:

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

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

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

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

再看看我们的问题:Tomcat如果使用默认的双亲委派类加载机制行不行?答案是不行的。为什么?

第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

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

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

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

7. Tomcat打破双亲委派机制

tomcat可以加载各种各样类型的war包,相互之间没有影响。因为tomcat打破了双亲委派机制。

1. 从JDK源码级别彻底刨析JVM类加载机制_第13张图片
1. 从JDK源码级别彻底刨析JVM类加载机制_第14张图片

如上图,上面的部分还是和原来一样,采用双亲委派机制,而黄色部分是tomcat第一部分自定义的类加载器,这部分主要加载tomcat包中的类,这一部分依然采用的是双亲委派机制,而tomcat第二部分自定义类加载器,正是这一部分,打破了类的双亲委派机制。

黄色第一部分加载器,在tomcat7及以前是tomcat自定义的三个类加载器,分别加载不同文件夹下的jar包,而到了tomcat7及以后,tomcat将这三个文件夹合并了,合并成了一个lib包,也就是现在看到的lib包。

Tomcat的几个主要类加载器:

  • commonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身和各个webapp访问;

  • catalinaClassLoader:tomcat容器中私有的类加载器,加载路径中的class对于webapp不可见的部分。

  • sharedClassLoader:各个webapps共享的类加载器,加载路径中的class对于所有的webapp都可见,但是对于tomcat容器不可见。

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

如上图中的委派关系中可以看出:CommonClassLoader能加载的类都是可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了共有的类库的共用,而被CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。 WebAppClassLoader可以使用SharedClassLoader类加载器的类,但各个WebAppClassLoader实例之间相互隔离。而jasperLoader的加载器范围仅仅是这个JSP文件编译出来的那一个.calss文件,他出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的热加载功能。

Tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?

答案是:违背了。

黄色的第二部分是java项目在打war包的时候, tomcat自动生成的类加载器, 也就是说 , 每一个项目打成一个war包, tomcat都会自动生成一个类加载器, 专门用来加载这个war包. 而这个类加载器打破了双亲委派机制. 我们可以想象一下, 假如这个webapp类加载器没有打破双亲委派机制会怎么样?

之前也说过,如果没有打破, 他就会委托父类加载器去加载, 一旦加载到了, 子类加载器就没有机会在加载了. 那么, spring4和spring5的项目想共存, 那是不可能的了.

所以, 这一部分他打破了双亲委派机制

这样一来, webapp类加载器不需要在让上级去加载, 他自己就可以加载对应war里的class文件. 当然了, 其他的基础项目文件, 还是要委托上级加载的.

下面我们来实现一个自定义的tomcat类加载器

8. 自定义tomcat的war包类加载器

如何打破双亲委派机制, 我们在上面已经写过一个demo了.

那么, 现在我有两个war包, 分处于不同的文件夹, tomcat如何使用各自的类加载器加载自己包下的class类呢?

我们来举个例子, 比如: 在我的本地目录下有两个文件夹, test和test1. 用这两个文件夹来模拟两个项目,在他们下面都有一个ding/People.class

其实这两个文件夹里面的People.class可以是不一样的,运行出来的语句不一样加以区分。我比较懒,就复制的一样的哈。

在这里插入图片描述

在这里插入图片描述

1. 从JDK源码级别彻底刨析JVM类加载机制_第15张图片

package ding;

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

/**
 * 自定义类加载器
 */
public class MyClassLoaderTest1{
    static class MyClassLoader extends ClassLoader {
        private String classPath;

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

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

        private byte[] loadByte(String name) throws Exception {
            //需要读取类的路径
            name = name.replaceAll("\\.", "/");
            //根据路径查找这个类
            FileInputStream file = new FileInputStream(classPath + "/" + name + ".class");
            int len = file.available();
            byte[] bytes = new byte[len];
            file.read(bytes);
            file.close();
            return bytes;
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t1 = System.nanoTime();
                    if (!name.startsWith("ding")){
                        c = this.getParent().loadClass(name);
                    }else {
                        c = findClass(name);
                    }
                    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 {
        //初始化自定义类加载器会先初始化父类ClassLoader,
        // 其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //E盘创建 tomcat-test/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class<?> clazz = classLoader.loadClass("ding.People");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("printf", null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());


        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
        //E盘创建 tomcat-test1/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class<?> clazz1 = classLoader1.loadClass("ding.People");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("printf", null);
        method1.invoke(obj1,null);
        System.out.println(clazz.getClassLoader().getClass().getName());

    }
}

其实这个和上面的那个打破双亲委托机制代码是一样的写法,唯一不一样的就是:

1. 从JDK源码级别彻底刨析JVM类加载机制_第16张图片

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

思考: tomcat自定义的类加载器中, 有一个jsp类加载器,jsp是可以实现热部署的, 那么他是如何实现的呢?

jsp其实是一个servlet容器, 由tomcat加载. tomcat会为每一个jsp生成一个类加载器. 这样每个类加载器都加载自己的jsp, 不会加载别人的. 当jsp文件内容修改时, tomcat会有一个监听程序来监听jsp的改动. 比如文件夹的修改时间, 一旦时间变了, 就重新加载文件夹中的内容.

具体tomcat是怎么实现的呢? tomcat自定义了一个thread, 用来监听不同文件夹中文件的内容是否修改, 如何监听呢? 就看文件夹的update time有没有变化, 如果有变化了, 那么就会重新加载.

jsp热部署也不是立刻就会看到效果,其他他也是有延迟的,这个延迟就是重新加载的过程。

你可能感兴趣的:(JVM性能调优,Tomcat,jvm,java,开发语言)