JVM 双亲委派

108 阅读6分钟

1.什么是双亲委派

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    //              -----??-----
     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) {
			//非bootstrapclassload,调用父类的loadclass
                        c = parent.loadClass(name, false);
                    } else {
			//parent为null说明是bootstrapclassload
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

		//没找到class,说明没被加载过,尝试加载
                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;
        }
    }

URLClassloader中的findclass方法:

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
	    //这是一个native方法,
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
			//Extclassloader直接返回null,appclassload返回非null
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
				//查找并定义class
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

URLClassLoader中调用的是父类Classloader的defineclass:

    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
	    //这里定义class,内部会调用一个native方法
            return defineClass(name, b, 0, b.length, cs);
        }
    }

综上总结下:

  1. bootstrapClassLoader 只加载jre/lib下的包
  2. extClassLoader只加载jre/lib/ext下的包
  3. appClassLoader只加载classpath下的包
  4. ClassLoader抽象类中有三个重要方法 loadclass,findclass(由子类URLClassLoader具体实现,ext和app都继承URLClassLoader),以及defineclass
  5. 自定义classloader时需要重写loadclass(打破双亲委派)以及findclass(自己加载类)方法
  6. 不同类加载器实例加载同一个限定名的类, 这两个类依旧是不一样的

2.为什么要破坏双亲委派

2.1 历史原因

现有loadclass接口,后有的双亲委派, 在双亲委派出现之前,有很多classload自定义loadclass,所以双亲委派为了兼容就留下了可以重写loadclass的口子(破坏双亲委派需要改造loadclass方法)

2.2 spi机制

spi机制是服务端提供接口,由客户端取实现,比如JDBC的driver类

加载driver类的classloader是BootstrapClassLoader, 当加载driver类时,其实现按道理也应该由BootstrapClassLoader加载才会满足双亲委派,但是实际这是不可能的, 所以DriverManager.getconnection时默认会委派appclassloader来加载第三方驱动

双亲委派是向上而行的,发起人是下层classloader,当上层无法加载时才会由发起人进行加载

DriverManager类是bootstrapClassLoader加载,是上层发起,需要加载第三方类,所以只能委派给底层的appclassloader,故破坏了双亲委派

2.3 热替换,热部署/多发布包隔离

双亲委派中一个类只能被一个类加载器加载一次,当类被修改时,是无法被再次加载的

所以如果要实现热部署,就一定要重定义一个classloader,自己去重新加载一个类

tomcat中会同时运行多个war包工程,这些工程按道理应该是隔离的,但当这些war包都包含同一个jar依赖包的不同版本(也就是说有相同的全限定名的类,这些类可能一致可能不一致),如果不打破双亲委派,这些类只能存在一份,多个war包工程就不隔离了,所以需要自定义classloader,打破双亲委派

3.自定义类加载器

3.1 不打破双亲委派

重写classloader的findclass方法,这种方式只能加载classpath外的class文件

加载流程依旧符合双亲委派, 一级一级向上查看myclassLoader->appclassloader->extclassloader->bootstrapclassloader

public class MyClassLoader extends ClassLoader {
    //自定义加载路径,在这个路径下使用自定义类加载器
    private String path;

    public MyClassLoader(String path, ClassLoader parent) {
        super(parent);
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            //加载文件进内存
            byte[] b = loadClassData(name);
            //调用classload二的defineclass
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] loadClassData(String name) throws IOException {
        name = path + name + ".class";
        InputStream is = null;
        ByteArrayOutputStream outputStream = null;
        try {
            is = new FileInputStream(new File(name));
            outputStream = new ByteArrayOutputStream();
            int i = 0;
            while ((i = is.read()) != -1) {
                outputStream.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
            if (is != null) {
                is.close();
            }
        }

        return outputStream.toByteArray();
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
	//这个路径下以及classpath路径下都有HelloWorld;
        MyClassLoader myClassLoader = new MyClassLoader("E:\\code-repo\\OTHER\\test\\file\\", MyClassLoader.class.getClassLoader());
        Object client = myClassLoader.loadClass("HelloWorld").newInstance();
	//输出false, 因为不是一个类加载器加载的
        System.out.println(client instanceof HelloWorld);

    }

3.2 打破双亲委派

重写classloader的loadclass和findclass方法

    @Override
    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) {
                /**
                 * 这部分逻辑可以自定义
                 * 可以先向上委派,再由自己加载
                 * 也可以根据包名来判断是否先由自己加载,再向上委派
                 * */
                try {
                    c = this.getParent().loadClass(name);
                } catch (ClassNotFoundException e) {

                }
                if (c == null) {
                    c = findClass(name);
                }
//                if (!name.startsWith("自定义包名")) {
//                    c = this.getParent().loadClass(name);
//                } else {
//                    c = findClass(name);
//                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

4. tomcat中的类加载器

4.1 tomcat类加载器结构图

  • Bootstrap 这个类加载器包含 Java 虚拟机提供的基本运行时类,以及系统扩展目录$JAVA_HOME/jre/lib/ext) 中存在的 JAR 文件中的任何类。 注意:一些 JVM 可能将其实现为多个类加载器,或者它可能根本不可见(作为类加载器)。实际就是bootstrapclassloader和extclassloader
  • System 这个类装入器通常是从CLASSPATH环境变量的内容初始化的。实际就是appclassloader.所有这些类对Tomcat内部类和web应用程序都是可见的。但是,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh 或 %CATALINA_HOME%\bin\catalina.bat)完全忽略了 CLASSPATH 环境变量本身的内容,而是从以下存储库构建系统类加载器:
    • $CATALINA_HOME/bin/bootstrap.jar 包含用于初始化Tomcat服务器的main()方法,以及它所依赖的类装入器实现类。
    • CATALINABASE/bin/tomcatjuli.jarorCATALINA_BASE/bin/tomcat-juli.jar** or **CATALINA_HOME/bin/tomcat-juli.jar 日志实现类。其中包括java.util.logging API的增强类,称为Tomcat JULI,以及Tomcat内部使用的Apache Commons Logging库的重命名副本。
    • $CATALINA_HOME/bin/commons-daemon.jar 来自Apache Commons Daemon项目的类。这个JAR文件不在catalina.bat|.sh脚本构建的CLASSPATH中,而是从bootstrap.jar的清单文件中引用。
  • Common 加载common.loader(conf/catalina.properties)下的classes文件、资源文件和jar包,这些类对Tomcat内部类和所有Web应用程序都是可见的。
  • Server 加载server.loader(conf/catalina.properties)下的classes文件、资源文件和jar包, 只对Tomcat内部可见,而对web应用程序完全不可见。
  • Shared 加载shared.loader(conf/catalina.properties)下的classes文件、资源文件和jar包,对所有web应用程序可见,并可用于在所有web应用程序之间共享代码。但是,对此共享代码的任何更新都需要Tomcat重新启动
  • WebappX 单个Tomcat实例中的每个Web应用程序创建的类加载器。加载Web应用程序的/Web-INF/Class目录中的所有未打包类和资源,再加上Web应用程序/Web-INF/lib目录下JAR文件中的类和资源,对此web应用程序都是可见的,而对其他类则不可见。