java基础之类运行与双亲委派机制简介

一 类加载运行过程

通过java命令运行某个类的main函数来启动程序时,首先需要通过类加载器将主类加载到JVM中;

源码:

java 复制代码
package com.ddu.jvm;

public class HelloWordHelper {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(1);
        user.setName("1岁");
        add(1, 2);
    }

    private static int add(int a, int b) {
        return a + b;
    }

    static class User {
        private String name;
        private int age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

通过Java命令执行代码的大体过程如下:

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

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

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

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

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

其中,主类在运行过程中如果需要使用到其他类,会逐步加载这些类;即jar包或war包里的类不是一次性全部加载的,是使用到时才会加载;

源码:

java 复制代码
package com.ddu.jvm;

public class HelloWordHelper {
    static {
        System.out.println("load HelloWordHelper...");
    }
    public static void main(String[] args) {
//        User user = new User();
//        user.setAge(1);
//        user.setName("1岁");
//        add(1, 2);

        new Order();
        System.out.println("load HelloWordHelper");
        // Address不会加载,除非这里执行new Address();
        Address address = null;
    }

    private static int add(int a, int b) {
        return a + b;
    }

    static class Order{
        static {
            System.out.println("loader Order ......");
        }
        public Order() {
            System.out.println("initial Order ... ");
        }
    }

    static class Address{
        static {
            System.out.println("loader Address ......");
        }
        public Address() {
            System.out.println("initial Address ... ");
        }
    }

    static class User {
        private String name;
        private int age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

运行结果:

java 复制代码
load HelloWordHelper...
loader Order ......
initial Order ... 
load HelloWordHelper

二 类加载器和双亲委派机制

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

  • 引导类加载器:负责加载支撑JVM运行的位于JRE安装目录lib目录下的核心类库,比如rt.jar、charsets.jar
  • 扩展类加载器:负载加载支持JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR包;
  • 应用程序类加载器:负载加载ClassPath路径下的类包,主要是加载开发人员自己开发的类;
  • 自定义类加载器:负责加载用户自定义路径下的类包;

类加载器示例:

java 复制代码
package com.ddu.jvm;

import sun.misc.Launcher;

import java.net.URL;

public class DduClassLoaderHelper {
    public static void main(String[] args) {
        // 预期是null,因为String是引导类加载器加载的,而引导类加载器是C++实现的,JVM内存中没有C++的实例
        System.out.println(String.class.getClassLoader());
        // DESKeyFactory类是JRE/lib/ext包下的,类加载器为ExtClassLoader
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        // 业务自己定义的类,类加载器为AppClassLoader
        System.out.println(DduClassLoaderHelper.class.getClassLoader().getClass().getName());

        System.out.println("---------------------------------");
        // 系统类加载器,即为AppClassLoader
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        // 系统类加载器的父加载器为扩展类加载器=>ExtClassLoader
        ClassLoader extClassLoader = systemClassLoader.getParent();
        // 扩展类加载器的父类加载器为系引导类加载器=>BootstrapClassLoader
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();

        System.out.println(systemClassLoader);
        System.out.println(extClassLoader);
        System.out.println(bootStrapClassLoader);
        System.out.println("--------BootstrapClassLoader加载如下的class文件-----------------------");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
        System.out.println("----------ExtClassLoader加载如下目录的class文件---------------------");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("----------AppClassLoader加载如下目录的class文件---------------------");
        System.out.println(System.getProperty("java.class.path"));
    }
}

运行结果:

java 复制代码
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
---------------------------------
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@506e1b77
null
--------BootstrapClassLoader加载如下的class文件-----------------------
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/classes
----------ExtClassLoader加载如下目录的class文件---------------------
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
----------AppClassLoader加载如下目录的class文件---------------------
C:\Program Files\Java\jdk1.8.0_333\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\rt.jar;D:\project\ddu-base\ddu-base\ddu-java\target\classes;D:\repository\org\springframework\boot\spring-boot-starter\2.1.4.RELEASE\spring-boot-starter-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot\2.1.4.RELEASE\spring-boot-2.1.4.RELEASE.jar;D:\repository\org\springframework\spring-context\5.1.6.RELEASE\spring-context-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-aop\5.1.6.RELEASE\spring-aop-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-beans\5.1.6.RELEASE\spring-beans-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-expression\5.1.6.RELEASE\spring-expression-5.1.6.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-autoconfigure\2.1.4.RELEASE\spring-boot-autoconfigure-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-starter-logging\2.1.4.RELEASE\spring-boot-starter-logging-2.1.4.RELEASE.jar;D:\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\repository\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\repository\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\repository\org\slf4j\jul-to-slf4j\1.7.26\jul-to-slf4j-1.7.26.jar;D:\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\repository\org\springframework\spring-core\5.1.6.RELEASE\spring-core-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-jcl\5.1.6.RELEASE\spring-jcl-5.1.6.RELEASE.jar;D:\repository\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\repository\org\springframework\boot\spring-boot-starter-test\2.1.4.RELEASE\spring-boot-starter-test-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-test\2.1.4.RELEASE\spring-boot-test-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-test-autoconfigure\2.1.4.RELEASE\spring-boot-test-autoconfigure-2.1.4.RELEASE.jar;D:\repository\com\jayway\jsonpath\json-path\2.4.0\json-path-2.4.0.jar;D:\repository\net\minidev\json-smart\2.3\json-smart-2.3.jar;D:\repository\net\minidev\accessors-smart\1.2\accessors-smart-1.2.jar;D:\repository\org\ow2\asm\asm\5.0.4\asm-5.0.4.jar;D:\repository\org\slf4j\slf4j-api\1.7.26\slf4j-api-1.7.26.jar;D:\repository\junit\junit\4.12\junit-4.12.jar;D:\repository\org\assertj\assertj-core\3.11.1\assertj-core-3.11.1.jar;D:\repository\org\mockito\mockito-core\2.23.4\mockito-core-2.23.4.jar;D:\repository\net\bytebuddy\byte-buddy\1.9.12\byte-buddy-1.9.12.jar;D:\repository\net\bytebuddy\byte-buddy-agent\1.9.12\byte-buddy-agent-1.9.12.jar;D:\repository\org\objenesis\objenesis\2.6\objenesis-2.6.jar;D:\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;D:\repository\org\hamcrest\hamcrest-library\1.3\hamcrest-library-1.3.jar;D:\repository\org\skyscreamer\jsonassert\1.5.0\jsonassert-1.5.0.jar;D:\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;D:\repository\org\springframework\spring-test\5.1.6.RELEASE\spring-test-5.1.6.RELEASE.jar;D:\repository\org\xmlunit\xmlunit-core\2.6.2\xmlunit-core-2.6.2.jar;D:\repository\org\projectlombok\lombok\1.18.22\lombok-1.18.22.jar;D:\soft\Intelijidea\lib\idea_rt.jar

2.1 类加载器初始化过程

类运行加载过程中,会创建JVM启动器实例sun.misc.Launcher,在Launcher构造方法内部,创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器),JVM默认使用Launcher的getClassLoader()方法返回的类加载器(即为AppClassLoader)加载我们的应用程序;

以下是sun.misc.Launcher部分源码:

java 复制代码
public class Launcher {
    private static URLStreamHandlerFactory factory = new 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;
    }

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

        try {
            // 构造应用类加载器,在构造过程中,将器父类加载器设置为ExtClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);

省略其他代码



public ClassLoader getClassLoader() {
  return this.loader;
}    

Launcher.ExtClassLoader.getExtClassLoader()实现:

java 复制代码
static class ExtClassLoader extends URLClassLoader {
        private static volatile ExtClassLoader instance;

        public static ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = ExtClassLoader.class;
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }

        private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
                    public ExtClassLoader run() throws IOException {
                        File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                        int var2 = var1.length;

                        for(int var3 = 0; var3 < var2; ++var3) {
                            MetaIndex.registerDirectory(var1[var3]);
                        }

                        return new ExtClassLoader(var1);
                    }
                });
            } catch (PrivilegedActionException var1) {
                throw (IOException)var1.getException();
            }
        }

        void addExtURL(URL var1) {
            super.addURL(var1);
        }

        public ExtClassLoader(File[] var1) throws IOException {
            // 此处设置当前类加载器的父类加载器为null
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
复制代码
Launcher.AppClassLoader.getAppClassLoader(extClassLoader)实现源码
java 复制代码
static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {
                public AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {

            // 此处var2即为设置父类加载器
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
省略其他代码

2.2 双亲委派机制

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

类加载其实就是一个双亲委派机制,加载某个类时,会先委托给父类加载器寻找加载目标类,父类加载器会继续委托给自己的父类加载器,直到当前类加载器的父类加载器为null(实际是C++实现的bootstapClassLoader)时,开始尝试加载当前类,如果所有的父类加载器都无法加载到当前类,则在自己的类加载路径加载当前类;

例如:HelloWordHelper类,

  • 逐层委托:首先会找应用类加载器加载,应用类加载器则会先委托给扩展类加载器加载,扩展类加载器会先委托给引导类加载器;
  • 自顶向下开始尝试加载目标类:
    • BootstapClassLoader加载不到HelloWordHelper,则退回至ExtClassLoader加载目标类;
    • ExtClassLoader加载不到目标类HelloWordHelper,则退回至AppClassLoader加载器尝试加载目标类;
    • AppClassLoader在class.path目录下加载到目标类,完成目标类加载;

简单点说:双亲委派机制就是儿子啃老,有事情先找老爹,老爹有事情先找爷爷,爷爷办不到老爹自己想办法,老爹实在办不到就儿子自己想办法了;

2.3 AppClassLoader源码是如何完成双亲委派机制的?

源码:

2.3.1 AppclassLoader实现了loadClass方法

最终会调用ClassLoader.loadClass方法

java 复制代码
static class AppClassLoader extends URLClassLoader {
       
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

            if (this.ucp.knownToNotExist(var1)) {
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }

                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
                // ClassLoader中实现了双亲委派机制
                return super.loadClass(var1, var2);
            }
        }
}

2.3.2 ClassLoader.loadClass方法实现双亲委派机制

java 复制代码
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);
                    // 如果当前类没有父类加载器,则委托给BootstapClassLoader尝试加载,最终调用native方法
                    } 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;
        }
    }

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

  • 沙箱安全机制:研发人员自己开发的java.lang.String.class类不会被加载,防止核心API库被随意修改;
  • 避免类的重复加载:当父类已经加载过该类时,就没必要子ClassLoader再加载一次,保证被加载类的唯一性;
    • 被加载类唯一性:加载器类+目标类

示例:

java 复制代码
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

2.5 委托机制的传播性

当一个ClassLoader加载一个类时,除非显示的使用另外一个ClassLoader,否则该类所依赖及引用的类也由这个ClassLoader载入;

2.6 自定义类加载器

自定义类加载器只需要加成java.lang.ClassLoader类,该类有两个核心方法:

  • loadClass(String,boolean):实现了双亲委派机制;
  • findClass:默认是空方法,自己类加载器时,主要是重写findClass方法;

示例:

java 复制代码
package com.ddu.jvm;

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

/**
 * 自定义类加载器
 */
public class DduClassLoaderHelper {

    public static void main(String[] args) {
        // d://uclazz/com/ddu/jvm/Order1.class
        DduClassLoader dduClassLoader = new DduClassLoader("D:/uclazz");
        try {
            Class<?> clazz = dduClassLoader.loadClass("com.ddu.jvm.Order1");
            Object order = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("print", null);
            method.invoke(order, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        }
    }

    static class DduClassLoader extends ClassLoader {
        private String classPath;

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

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = null;
            byte[] data = null;
            try {
                // d://uclazz/com/ddu/jvm/Order1.class
                fis = new FileInputStream(classPath + "/" + name + ".class");
                int length = fis.available();
                data = new byte[length];
                fis.read(data);
            } catch (Exception e) {
                throw new RuntimeException("加载class文件异常!");
            } finally {
                if (Objects.nonNull(fis)) {
                    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) {
                throw new RuntimeException(e);
            }
        }

    }
}

Order1源码:

放置目录:D:\uclazz\com\ddu\jvm

自己javac Order1.java

java 复制代码
package com.ddu.jvm;

import java.io.File;

public class Order1 {

    public void print(){
        File file = new File("");
        System.out.println("d: uclazz order load path:"+file.getAbsoluteFile());
    }
}

运行结果:

java 复制代码
d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader

2.7 tomcat是如何打破双亲委派机制的?

2.7.1 tomcat作为web容器,需要解决什么问题?

  1. 一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个三方包的不同版本,因此需要保证每个应用程序的依赖类库都是独立的,保证相互隔离;
  2. 部署在同一个web容器中的相同类库相同的版本可以共享,否则如果一个web容器下部署了n个应用程序,那么要用n份相同的依赖类库加载到虚拟机;
  3. web容器本身也需要依赖三方类库,容器依赖的三方类库与应用程序的依赖三方类库不能混淆,基于安全考虑,应该让容器的类库和程序的类库隔离开来;
  4. web容器要支持jsp的修改,而jsp文件最终是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是很常见的事情,web容器需要支持jsp修改后不重启就可以生效;也就是大家常说的:jsp热加载;

2.7.2 tomcat如果使用默认的双亲委派加载机制行不行?

  • 答案:不行;
  • 原因:
  1. 问题1,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,同一个类全限定名同一个类加载器仅加载一次;
  2. 问题2,默认类加载器是可以实现的,因为它的职责就是保证唯一性
  3. 问题3和问题1是一样的问题;
  4. 问题4,如何实现jsp的热加载?
    1. jsp文件最终都会转成class文件,虽然内容发生了修改,但是类名还是一样的,类加载器会直接取方法区中已经存在的类信息,修改后的jsp是不会重新加载的,所以应该如何实现呢?思路:卸载掉当前jsp文件的类加载器,也就是说每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载,重新创建类加载器,重新加载jsp文件;

2.7.3 tomcat自定义类加载器详解

tomcat几个主要的类加载器:
  • CommonLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身以及各个Webapp程序访问;
  • CatalinaLoader:tomcat容器私有的类加载器,加载路径中的class对Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于tomcat容器不可见;
  • WebappClassLoader:各个WebApp私有的类加载器,加载路径中的class仅对当前webapp可见;

从上图的委派关系可以看出,

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公共类库的共用;
  • CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方互相隔离;
  • WebappClassLoader可以使用SharedClassLoader加载到的类,但是各个WebappClassLoader实例之间互相隔离;
  • JasperLoader加载范围仅仅是这个JSP文件所编译出来的那一个class文件,当Web容器检测到JSP文件被修改了,会替换掉目前的JasperLoader实例,并通过重新加载一个新的Jsp类加载器来实现JSP文件的热加载功能;

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

  • 答案是违背了。因为tomcat为了实现隔离性,没有遵循双亲委派机制,每个WebappClassLoader加载自己目录下的class文件,不会向上委托给父类加载器,因此是打破了双亲委派机制;

2.7.4 tomcat打破双亲委派的简单实现

继承ClassLoader类,重写protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException方法,加载指定类时不再向上委托给父类加载器;

代码:

java 复制代码
package com.ddu.jvm;

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

/**
 * 自定义类加载器
 */
public class DduClassLoaderHelper {

    public static void main(String[] args) {
        // d://uclazz/com/ddu/jvm/Order1.class
        DduClassLoader dduClassLoader2 = new DduClassLoader("D:/uclazz");
        try {
            Class<?> clazz2 = dduClassLoader2.loadClass("com.ddu.jvm.Order2");
            Object order2 = clazz2.newInstance();
            Method method1 = clazz2.getDeclaredMethod("print", null);
            method1.invoke(order2, null);
            System.out.println(clazz2.getClassLoader().getClass().getName());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        }

        // d://uclazz/com/ddu/jvm/Order1.class
        DduClassLoader dduClassLoader = new DduClassLoader("D:/uclazz");
        try {
            Class<?> clazz = dduClassLoader.loadClass("com.ddu.jvm.Order1");
            Object order = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("print", null);
            method.invoke(order, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        }
    }

    static class DduClassLoader extends ClassLoader {
        private String classPath;

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

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = null;
            byte[] data = null;
            try {
                // d://uclazz/com/ddu/jvm/Order1.class
                fis = new FileInputStream(classPath + "/" + name + ".class");
                int length = fis.available();
                data = new byte[length];
                fis.read(data);
            } catch (Exception e) {
                throw new RuntimeException("加载class文件异常!");
            } finally {
                if (Objects.nonNull(fis)) {
                    fis.close();
                }
            }
            return data;
        }

        /**
         * 如果只是为了自定义类加载器,直接重写当前方法就可以
         *
         * @param name The <a href="#name">binary name</a> of the class
         * @return
         * @throws ClassNotFoundException
         */
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        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 still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 自定义的类不再委托给父类加载
                    if(name.startsWith("com.ddu.jvm")){
                        c = findClass(name);
                        // 非自定义的类还是按双亲委派机制加载
                    }else {
                        c = this.getParent().loadClass(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 复制代码
2 d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader
d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader

Order源码:

两个类自己修改打印日志的内容然后重新编译就可以使用了

java 复制代码
package com.ddu.jvm;

import java.io.File;

public class Order {

    public void print(){
        File file = new File("");
        System.out.println("project ddu order load path:"+file.getAbsoluteFile());
    }
}

2.7.5 tomcat的JasperLoader热加载简单实现

原理:后台启动线程监听JSP文件变化,如果文件内容发生了变化,找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot,下次gc的时候会被垃圾回收;

具体tomcat实现的方式参考:Tomcat热部署与热加载_smarttomcat开启热部署-CSDN博客

相关推荐
爱编程— 的小李8 分钟前
结构体(c语言)
c语言·开发语言
卓越小Y14 分钟前
配置jellyfin docker 硬件加速
java·spring cloud·docker
白萝卜弟弟16 分钟前
【JAVA】正则表达式中的捕获组和非捕获组
java·正则表达式
fathing20 分钟前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#
袁庭新37 分钟前
LuaRocks如何安装数据库驱动?
java·数据库·redis·lua·luarocks·袁庭新
前端青山42 分钟前
webpack指南
开发语言·前端·javascript·webpack·前端框架
hummhumm1 小时前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
nukix1 小时前
Mac Java 使用 tesseract 进行 ORC 识别
java·开发语言·macos·orc
月光光心慌慌。1 小时前
新日撸java三百行` 新手小白java学习记录 `Day1
java
蘑菇丁1 小时前
ranger-kms安装
java·ide·eclipse