首页 > 代码库 > java类加载器不完整分析

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实现的,有的地方也叫SystemClassLoader。负责加载系统类路径-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 {
                    // Bootstrap是无法被访问的,所以ext的parent必然null
                    // 此时直接用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传入构造函数)的情况下,默认采用系统类加载器()。对应的无参默认构造函数实现如下:

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,并赋给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的底层实现机制以及熟悉反射的实现方式。我个人的风格就是知其然知其所以然,在我理解范围内的知识我都有兴趣去研究。之前总是花一整段时间去啃下难点后就置之不理了,工作后才养成这种常记笔记的习惯,自己总结梳理后的确比看别人的文章要来得更深刻更透彻,望继续保持!

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    java类加载器不完整分析