• java类加载器不完整分析


    虽然之前也看过jvm相关的书籍,但是都是概念层次上的理解。今天特地花一天时间研究了下类加载器,感觉上是没有那么生疏了,但也只是冰山一角,索性就不完整地分析一番吧。内容有些长,可使用目录快速查阅。

    类加载器

      简单说下JVM预定义的三种类型的类加载器,这个也算是老生常谈了。当JVM启动一个项目的时候,它将缺省使用以下三种类型的类加载器:
    1. 启动(Bootstrap)类加载器:负责装载<Java_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包。由native方法实现加载过程,程序无法直接获取到该类加载器,无法对其进行任何操作。
    2. 扩展(Extension)类加载器:扩展类加载器由sun.misc.Launcher.ExtClassLoader实现的。负责加载<Java_Home>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库。程序可以访问并使用扩展类加载器。
    3. 系统(System)类加载器:系统类加载器是由sun.misc.Launcher.AppClassLoader实现的,也叫应用程序类加载器。负责加载系统类路径-classpath-Djava.class.path变量所指的目录下的类库。程序可以访问并使用系统类加载器。

    双亲委派类加载机制

    类加载器的父子关系

    三种类加载器的父子关系如图所示
    类加载父子关系

    注意这儿的父子并不是继承的意思,它们都是ClassLoader抽象类的实现,因此都含有一个ClassLoader parent成员变量,该变量指向其父加载器。

    双亲委派源码实现

    委派关系也被称为代理,我们来看看代码,loadClass是抽象类ClassLoader中的类加载的核心方法。

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 若本加载器之前是否已加载过,直接取缓存,native方法实现
                Class c = findLoadedClass(name);
                if (c == null) {
                    try {
                        // 只要有父加载器就先委派父加载器来加载
                        if (parent != null) {
                            // 注意此处递归调用
                            c = parent.loadClass(name, false);
                        } else {
                            // ext的parent为null,因为Bootstrap是无法被程序被访问的,默认parent为null时其父加载器就是Bootstrap
                            // 此时直接用native方法调用启动类加载加载,若找不到则抛异常
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // 对ClassNotFoundException不做处理,仅用作退出递归
                    }
    
                    if (c == null) {
                        // 如果父加载器无法加载那么就在本类加载器的范围内进行查找
                        // findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果
                        c = findClass(name);
                    }
                }
                // 是否解析,默认false
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }

    可以看出所谓的双亲委派的本质就是这两句递归代码:

    if (parent != null) {
        c = parent.loadClass(name, false);
    }

    加载成功就得到Class对象c,失败就抛异常然后前一级方法用catch抓住并忽略,再进行当前类加载器的findClass()操作,如此反复。

    注意
    1. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
    2. 类加载后将进入连接(link)阶段,它包含验证、准备、解析,resolve参数决定是否执行解析阶段,jvm规范并没有严格指定该阶段的执行时刻
    3. 由于先使用findLoadedClass()查找缓存,相同的类只会被加载一次

    用户自定义类加载器

    当你自己写一个类实现了ClassLoader后,那么它就是用户自定义类加载器了。实例化自定义类加载器时,若不指定父类加载器(不把父ClassLoader传入构造函数)的情况下,默认采用系统类加载器(AppClassLoader)。对应的无参默认构造函数实现如下:

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

    它将调用有参构造函数,将getSystemClassLoader()取到的系统类加载器作为parent传入(最后一节详述)。因此用户自定义类加载器也可以通过双亲委派的方式获取到那3个类加载器加载的类对象了。

    当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式。

    使用自定义类加载器获取到的Class对象需通过newInstance()获取实例,要比较具有相同类全限定名的两个Class对象是否是同一个,取决于是否是同一类加载器加载了它们,也就是调用defineClass()的那个类加载器,而非之前委派的类加载器。

    常用方法分析

    java.lang.Class对象的方法

    Class<?> forName(……)

    这是手动加载类的常见方式,在Class类中有两个重载:

    • public static Class<?> forName(String className)
    • public static Class<?> forName(String name, boolean initialize,
      ClassLoader loader)

    第二个构造函数指定了父类加载器,这儿可能要有疑问了,第一个方法默认使用哪个类加载器来加载的呢?我们来看下具体实现:

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        // 使用native方法获取调用类的Class对象
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

    其中getClassLoader(caller)设置了所使用的类加载器,继续看其实现:

     static ClassLoader getClassLoader(Class<?> caller) {
         if (caller == null) {
             return null;
         }
         return caller.getClassLoader0();
     }
    }

    这段代码的官方注解是“返回caller的类加载器”,即native方法getClassLoader0()返回调用者的类加载器。也就是说假设在A类里执行forName(String className),那么所使用的ClassLoader就是加载A的ClassLoader。

    提示
    forName0()本质还是调用ClassLoaderloadClass()来加载类。

    ClassLoader getClassLoader()

    该方法用于获取加载某Class对象的类加载器,可是通过实例或类对象来获取:

    • (new A()).getClass().getClassLoader()
    • A.class.getClassLoader()

    各种获取类信息的方法

    反射得到Class对象后通过以下方法获取类信息:

    Field[] getDeclaredFields()

    Class[] getDeclaredClasses()

    Method[] getDeclaredMethods()

    等等

    详情可查阅javadoc或查看源码

    java.lang.ClassLoader对象的方法

    ClassLoader getParent()

    获取父ClassLoader

    Class loadClass(String)

    显式调用该方法来进行类加载,传入类全限定名

    URL getResource(String)

    获取具有给定名称的资源定位符。资源可以是任何数据,名称须以“/”分离路径名。实际调用findResource()方法,该方法无实现,需子类继承实现。

    InputStream getResourceAsStream(String)

    获取可以读取资源的InputStream输入流,实际上就是用上面的方法获取到URL后调用url.openStream()得到 InputStream。

    ClassLoader getSystemClassLoader()

    这是一个静态方法,通过ClassLoader.getSystemClassLoader()便可获取到系统类加载器AppClassLoader, 和调用类无关。具体实现见最后一小节。

    URLClassLoader

    概述

    ClassLoader只是一个抽象类,很多方法是空的需要自己去实现,比如 findClass()findResource()等。而java提供了java.net.URLClassLoader这个实现类,适用于多种应用场景。

    之前提到的AppClassLoaderExtClassLoader都是URLClassLoader的子类,自定义类加载器推荐直接继承它。

    来看下javadoc中的描述:

    该类加载器用于从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。如果不是以该字符结束,则认为该URL指向一个JAR文件。

    构造函数

    URLClassLoader接受一个URL数组为参数,它将在这些提供的路径下加载所需要的类,对应的主要构造函数有

    • public URLClassLoader(URL[] urls)
    • URLClassLoader(URL[] urls, ClassLoader parent)

    getURLs()方法

    使用URL[] getURLs()方法可以获取URL路径,参考代码:

    public static void main(String[] args) {
        URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }
    // file:/D:/Workbench/Test/bin/

    加载方式

    findClass()中其使用了URLClassPath类中的Loader类来加载类文件和资源。URLClassPath类中定义了两个Loader类的实现,分别是FileLoaderJarLoader类,顾名思义前者用于加载目录中的类和资源,后者是加载jar包中的类和资源。Loader类默认已经实现getResource()方法,即从网络URL地址加载jar包然后使用JarLoader完成后续加载,而两个实现类不过是重写了该方法。

    你们可能要问URLClassPath是如何选择使用正确的Loader的呢?答案是——根据URL格式而定。下面是删减过的核心代码,简单易懂。

    private Loader getLoader(final URL url)
    {
        String s = url.getFile();
        // 以"/"结尾时,若url协议为"file"则使用FileLoader加载本地文件
        // 否则使用默认的Loader加载网络url
        if(s != null && s.endsWith("/"))
        {
            if("file".equals(url.getProtocol()))
                return new FileLoader(url);
            else
                return new Loader(url);
        } else {
            // 非"/"结尾则使用JarLoader
            return new JarLoader(url, jarHandler, lmap);
        }
    }

    getSystemClassLoader()方法的实现

    追溯getSystemClassLoader()的源码可以发现其实质上是通过sun.misc.Launcher实例获取返回其成员变量loader的。那这个loader是何时赋值的呢?我们来看下它的构造函数(删减了不相关的内容):

      public Launcher()
      {
          ExtClassLoader extclassloader;
          try
          {
          // 创建并初始化扩展类加载器ExtClassLoader
              extclassloader = ExtClassLoader.getExtClassLoader();
          }
          catch(IOException ioexception)
          {
              throw new InternalError("Could not create extension class loader");
          }
          try
          {
              // 创建并初始化系统类加载器AppClassLoader,设置其父类加载器为ext,最后传给loader
              loader = AppClassLoader.getAppClassLoader(extclassloader);
          }
          catch(IOException ioexception1)
          {
              throw new InternalError("Could not create application class loader");
          }
          // 默认将线程上下文类加载器设置为AppClassLoader
          // 相关信息见另一篇博文
          Thread.currentThread().setContextClassLoader(loader);
      }

    可以看到Launcher初始化时创建生成了ExtClassLoaderAppClassLoader,并将线程上下文类加载器默认设置为了AppClassLoader。虽然没去看jvm的源码,但我推测jvm可能就是通过创建Launcher实例来完成扩展和系统类加载器的创建的,而启动(Bootstrap)类加载器的创建则是另外调用本地方法完成的。

    很明显,getSystemClassLoader()返回的loader就是AppClassLoader无误,这儿我们也发现了线程上下文类加载器赋值处,具体有关线程上下文类加载器的学习请参考底部的另一篇博文。

    总结

    通常需要你自己写类加载器的场景不多,但通过上述对类加载器的分析研究至少可以让你了解jvm的底层实现机制以及熟悉反射的实现方式。我个人的风格就是知其然知其所以然,在我理解范围内的知识我都有兴趣去研究。之前总是花一整段时间去啃下难点后就置之不理了,工作后才养成这种常记笔记的习惯,自己总结梳理后的确比看别人的文章要来得更深刻更透彻,望继续保持!





    延伸阅读:真正理解线程上下文类加载器(多案例分析)

  • 相关阅读:
    Flutter图片选择 image_picker(官方)插件使用详解
    androidstudo如何跨越这个厚厚的墙,亲测有效 Could not resolve com.android.tools.build:gradle:
    qwq。。胡诌qwq
    关于很狗的军训qwq
    Leetcode每日一题 503.下一个更大元素II
    C++ 关于volatlie
    C++虚成员函数与动态联编
    graphics pipeline
    pointer or function
    线段树
  • 原文地址:https://www.cnblogs.com/yangcheng33/p/6557320.html
Copyright © 2020-2023  润新知