class(三)ClassLoader类加载机制

  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() {
                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() {
                    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类加载机制)