美文网首页Java基础
class(三)ClassLoader类加载机制

class(三)ClassLoader类加载机制

作者: Timmy_zzh | 来源:发表于2020-12-10 22:16 被阅读0次
    1. 字节码文件是如何被加载的?以及加载时机
    2. java中的已有的类加载器
    3. 双亲委派机制
    4. 通过自定义类加载器加载磁盘的字节码文件实现热修复功能
    前言

    之前介绍了Java字节码文件(.class)的格式。

    • 一个完整的Java程序由多个.class文件组成,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。
    • 而负责加载这些.class文件的就是类加载器-ClassLoader

    Java中的类何时被加载器加载

    • 在Java程序启动的时候,并不会一次性加载程序中所有的.class文件,而是在程序运行过程中,动态地加载相应的类到内存中(方法区)
    • 通常情况下Java程序中的.class文件会在以下2种情况被ClassLoader主动加载到内存中
      • 调用类构造器
      • 调用类中的静态(static)变量或者静态方法

    Java中的ClassLoader

    • JVM中自带3个类加载器
      • 启动类加载器 BootstrapClassLoader
      • 扩展类加载器 ExtClassLoader
      • 系统加载器 APPClassLoader
    系统加载器 APPClassLoader
        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<Launcher.AppClassLoader>() {
                    public Launcher.AppClassLoader run() {
                        URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                        return new Launcher.AppClassLoader(var1x, var0);
                    }
                });
            }
    
    • AppClassLoader主要加载系统属性“java.class.path”配置下类文件,也就是环境变量CLASS_PATH配置的路径
    • 因此AppClassLoader是面向用户的类加载器,我们编写的代码以及使用的第三方jar包通常都是由他来加载的
    扩展类加载器 ExtClassLoader
            private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
                try {
                    return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                        public Launcher.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 Launcher.ExtClassLoader(var1);
                        }
                    });
                } catch (PrivilegedActionException var1) {
                    throw (IOException)var1.getException();
                }
            }
            
            private static File[] getExtDirs() {
                String var0 = System.getProperty("java.ext.dirs");
                File[] var1;
                ...
                return var1;
            }
    
    • ExtClassLoader主要加载系统属性"java.ext.dirs"配置下类文件。
    启动类加载器 BootstrapClassLoader
    • BootstrapClassLoader不是用Java代码实现的,而是由C/C++语言编写的,本身属于虚拟机的一部分。
    • 其加载系统属性 “sun.boot.class.path”配置下类文件。

    代码实现:

        public static void main(String[] args) {
            System.out.println(System.getProperty("java.class.path"));
            System.out.println("------------------------");
            System.out.println(System.getProperty("java.ext.dirs"));
            System.out.println("------------------------");
            System.out.println(System.getProperty("sun.boot.class.path"));
        }
    

    双亲委派模式

    问题:既然JVM中已经有了3中ClassLoader,那么我们编写的.class文件到底该使用哪一个类加载器去加载呢?

    答案是:双亲委派模式

    • 双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载。

      • 也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程
    • class类的加载方法在ClassLoader的loadClass方法中实现

    public abstract class ClassLoader {
        
        protected ClassLoader(ClassLoader parent) {
            this(checkCreateClassLoader(), parent);
        }
        
        private ClassLoader(Void var1, ClassLoader parent) {
            this.classes = new Vector();
            this.defaultDomain = new ProtectionDomain(new CodeSource((URL)null, (Certificate[])null), (PermissionCollection)null, this, (Principal[])null);
            this.packages = new HashMap();
            this.nativeLibraries = new Vector();
            this.defaultAssertionStatus = false;
            this.packageAssertionStatus = null;
            this.classAssertionStatus = null;
            this.parent = parent;
            if (ClassLoader.ParallelLoaders.isRegistered(this.getClass())) {
                this.parallelLockMap = new ConcurrentHashMap();
                this.package2certs = new ConcurrentHashMap();
                this.domains = Collections.synchronizedSet(new HashSet());
                this.assertionLock = new Object();
            } else {
                this.parallelLockMap = null;
                this.package2certs = new Hashtable();
                this.domains = new HashSet();
                this.assertionLock = this;
            }
        }
        
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
        protected Class<?> loadClass(String name, boolean resolve) {
            
            synchronized(getClassLoadingLock(name)) {
                Class c = findLoadedClass(name);
                if (c == null) {
                    long var5 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                    }
                    if (c == null) {
                        long var7 = System.nanoTime();
                        c = findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }
    
    • 代码解析

      • findLoadedClass()方法判断该Class是否已加载,如果已加载,则直接将该Class返回
      • 如果该Class没有被加载过,则判断parent是否为空,如果不为空则将类加载的任务委托给parent,调用parent的loadClass()方法
        • parent也是ClassLoader类型,作为构造函数入参传入
      • 如果parent == null,则直接调用BootstrapClassLoader加载该类
      • 如果parent或者BootstrapClassLoader都没有加载成功,则调用当前ClassLoader的findClass方法继续尝试加载
    • parent属性

      • 可以看出,在每一个ClassLoader中都有一个ClassLoader类型的parent引用,并且在构造器中传入值。
      • 从源码中可知,AppClassLoader传入的parent是ExtClassLoader,而ExtClassLoader并没有传入任何parent,也就是null
    举例

    执行下面的代码,_2ClassLoader类是如何加载的呢?

        _2ClassLoader demo = new _2ClassLoader();
    
    • 默认情况下,JVM首先使用AppClassLoader去加载_2ClassLoader类

      1. AppClassLoader将加载任务委派给它的父类加载器ExtClassLoader
      2. ExtClassLoader的parent为null,所以直接将加载任务委派给BootstrapClassLoader
      3. BootstrapClassLoader在jdk/lib目录下无法找到Test类,因此返回的Class为null
      4. 因为parent和BootstrapClassLoader都没有成功加载_2ClassLoader类,所有AppClassLoader会调用自身的findClass()方法来加载 _2ClassLoader类
    • 最终_2ClassLoader类是被AppClassLoader加载到内存中的,可通过如下代码验证:

        private static void test3() {
            ClassLoader classLoader = _2ClassLoader.class.getClassLoader();
            System.out.println("classLoader is "+classLoader);
    
            ClassLoader parent = classLoader.getParent();
            System.out.println("parent is "+parent);
    
            ClassLoader bootstrp = parent.getParent();
            System.out.println("bootstrp is "+bootstrp);
        }
    
    打印结果:
    classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
    parent is sun.misc.Launcher$ExtClassLoader@42a57993
    bootstrp is null
    

    注意:双亲委派 机制只是java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。

    • 如果想保持双亲委派模式,就应该重写findClass(name)方法
    • 如果想破坏双亲委派模型,可以重写loadClass(name)方法

    自定义ClassLoader

    • 加入我们要加载在磁盘上的一个.class文件(该文件可以是网络下载下来的)
    public class ClassTest {
        public void printTest(){
            System.out.println("ClassTest -- print 111");
        }
    }
    
    • 自定义ClassLoader从磁盘加载class文件,获取文件字节流,并加获取Class对象
    public class DiskClassLoader extends ClassLoader {
    
        private String filePath;
    
        public DiskClassLoader(String path) {
            this.filePath = path;
        }
    
        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            String classPath = filePath + className + ".class";
            byte[] classBytes = null;
            try {
                Path path = Paths.get(new URI(classPath));
                classBytes = Files.readAllBytes(path);
            } catch (URISyntaxException | IOException e) {
                e.printStackTrace();
            }
            return defineClass(className, classBytes, 0, classBytes.length);
        }
    }
    
    • 使用自定义的ClassLoader加载字节码文件,并反射生成类对象
    public class ClassLoaderUse {
    
        public static void main(String[] args) {
            System.out.println("111");
            String filePath = "E:/Study/";
            DiskClassLoader diskClassLoader = new DiskClassLoader(filePath);
            try {
                Class<?> aClass = diskClassLoader.loadClass("ClassTest");
                if (aClass != null) {
                    Object obj = aClass.newInstance();
                    Method method = aClass.getMethod("printTest", null);
                    method.invoke(obj, null);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 动态加载.class文件的思路,经常被用作热修复和插件化开发的框架中,流程如下:
      • 客户端从服务端下载一个加密的.class文件,然后再本地通过事先定义好的加密方式进行解密
      • 再使用自定义ClassLoader动态加载解密后的.class文件,并动态调用相应的方法

    相关文章

      网友评论

        本文标题:class(三)ClassLoader类加载机制

        本文链接:https://www.haomeiwen.com/subject/gbqdgktx.html