JVM - ClassLoader

1. 概述

类加载器实际定义了类的namespace。

package java.lang;

public abstract class ClassLoader {
     public Class loadClass(String name);

     protected Class defineClass(byte[] b);

     public URL getResource(String name);

     public Enumeration getResources(String name);

     public ClassLoader getParent();//
}

2.类加载方式之当前类加载器和指定类加载器

类的加载只有两种加载方式,即当前类加载器加载(JVM自动行为,无法干预)和指定类加载器加载(自己指定类加载器进行加载)。

2.1 当前类加载器

class A{
   B b;
}  

B的加载会使用A的类的类加载器进行加载,A的类加载器就是当前类加载器。这种类加载方式是JVM自动进行的,无法干预。

2.2 指定类加载器

代码指定类加载器进行加载

3. 类加载器分类:定义类加载器和初始类加载器

  • 定义类加载器
    类的真实加载器,即通过class.getClassLoader()获得的类加载器。
  • 初始类加载器
    类最先是由初始类加载器进行加载,初始类加载器并不一定是真正最后加载到类的加载器。

4. 各种类加载器

  1. BootStarpClassLoader
    Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
  2. ExtClassLoader
    Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
  3. AppClassLoader
    系统类加载器,加载classpath下的类库。该类是Launcher类下的内部类(包访问权限),所以无法new 一个系统类加载器
    通过ClassLoader.getSystemClassLoader()能获得该加载器,该加载器是单例的。
    类加载器是有父类加载器的,默认的父类加载器是系统类加载器
  4. 线程上下文类加载器
    Main线程的上下文类加载器是系统类加载器,线程的默认上下文类加载器是父线程的上下文类加载器。
  • 得到上下文类加载器
    public ClassLoader getContextClassLoader()
  • 设置上下文类加载器
    public void setContextClassLoader(ClassLoader cl)
    4.1 框架中一般怎么用上下文类记载器
    试想: 如果一个JNDI的提供方,或者JAXP的提供方,他们的SPI是通过bootstrap加载的,但是他们的实现类必须通过应用ClassLoader甚至是更下层的ClassLoader来加载。那么在其初始化的过程中,需要考虑如果获取到部署了SPI实现的ClassLoader,而给出的方案是使用ContextClassLoader。比如。 在javax.xml.parsers.DocumentBuilderFactory中。

5. ClassLoader.loadClass

双亲制:

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) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    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;
        }
    }

6. Class.forName和ClassLoader.loadClass过程

6.1 Class.forName

这里讨论的Class.forNamepublic static Class<?> forName(String className) throws ClassNotFoundException
这里讨论的ClassLoader.loadClasspublic Class<?> loadClass(String name) throws ClassNotFoundException

Class.forName是根据给定的类型全名从当前类加载器中加载指定的类型。
实现代码:

    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

Reflection.getCallerClass(),获取到调用Class.forName方法的类,隐含意义就是当前类加载器。加载的逻辑在native方法forName0中定义,也就是forName进行的类加载行为已经脱离了Java代码的控制范围,进入到了Java运行时环境把控的阶段。
以下是JDK实现的部分代码:
Class.c中对应的实现逻辑:

JNIEXPORT jclass JNICALL
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
                              jboolean initialize, jobject loader)
{
    // 略
    cls = JVM_FindClassFromClassLoader(env, clname, initialize,
                                       loader, JNI_FALSE);
 done:
    if (clname != buf) {
        free(clname);
    }
    return cls;
}

实现细节在JVM_FindClassFromClassLoader中定义,可以看到调用Class.forName会使用JVM_FindClassFromClassLoader这个函数来进行类型加载,我们需要注意的是clname和loader这两个变量,一个是类的全限定名,另一个是ClassLoader,而Class.forName所使用的ClassLoader是当前类加载器。
在jvm.cpp中FindClassFromClassLoader的对应实现是:

jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
  // 略
}

SystemDictionary,系统字典,这个数据结构是保存Java加载类型的数据结构,如下图所示。


image.png

上图黑色边框中的内容就是SystemDictionary,它是以类的全限定名再加上类加载器作为key,进而确定Class引用。
当在代码中调用Class.forName(String name)或者由运行时Java进行类加载,比如:

          public void m() {
               B b = new B();
          }

对类型B的加载,就是运行时Java进行的类加载。
类型加载时,以ClassLoader和需要加载的类型全限定名作为参数在SystemDictionary中进行查询,如果能够查询到则返回。如果无法找到,则调用loader.loadClass(className)进行加载,这一步将进入到Java代码中。

对于loadClass而言,基本等同于loader.defineClass(loader.getResource(file).getBytes()),它做了两件事,

  • 第一件,通过资源定位到类文件,
  • 第二件,将类文件的字节流数组传递给defineClass进行构造Class实例。而defineClass将再一次派发给运行时Java进行执行。
    字节流数组经过ClassFileParser进行处理之后,生成了Class实例,在返回Class实例前,Java将name、loader和class的对应关系添加到SystemDictionary中,这样在后续其他类型的加载过程中,就能够快速找到这些类型,避免无谓的defineClass过程。

一个类加载的过程,在运行时Java(JVM)和java代码之间来回切换,有点复杂,我们画一个简单的图来描述主要过程,由于原有的类加载过程中还要处理并发问题,我们将这些内容都去掉,只观察类型加载的主要流程,如下图所示。


image.png
  1. 调用Class.forName(className)方法,该方法会调用native的JVM实现,调用前该方法会确定准备好需要加载的类名以及ClassLoader,将其传递给native方法
  2. 进入到JVM实现后,首先会在SystemDictionary中根据类名和ClassLoader组成hash,进行查询,如果能够命中,则返回
  3. 如果加载到则返回
  4. 如果在SystemDictionary中无法命中,将会调用Java代码:ClassLoader.loadClass(类名),这一步将委派给Java代码,让传递的ClassLoader进行类型加载
  5. 以URLClassLoader为例,ClassLoader确定了类文件的字节流,但是该字节流如何按照规范生成Class对象,这个过程在Java代码中是没有体现的,其实也就是要求调用ClassLoader.defineClass(byte[])进行解析类型,该方法将会再次调用native方法,因为字节流对应Class对象的规范是定义在JVM实现中的
  6. 进入JVM实现,调用SystemDictionary的resolve_stream方法,接受byte[],使用ClassFileParser进行解析
  7. SystemDictionary::define_instance_class
  8. 如果类型被加载了,将类名、ClassLoader和类型的实例引用添加到SystemDictionary中
  9. 返回
  10. 返回
  11. 从Java实现返回到Java代码的defineClass,返回Class对象
  12. 返回给loadClass(Classname)方法
  13. 返回给Java实现的SystemDictionary,因为在resolve_class中调用的ClassLoader.loadClass。这里会做出一个判断,如果加载Class的ClassLoader并非传递给resolve_class的ClassLoader,那么会将类名、传递给resolve_class的ClassLoader以及类型的实例引用添加到SystemDictionary中
  14. 返回给Class.forName类型实例

上述的过程比较复杂,但是简化理解一下它所做的工作,我们将SystemDictionary记作缓存,Class.forName或者说Java默认的类型加载过程是:*

  1. 首先根据ClassLoader,我们称之为initialClassLoader和类名查找缓存,如果缓存有,则返回;
  2. 如果缓存没有,则调用ClassLoader.loadClass(类名),加载到类型后,保存<类名,真实加载类的ClassLoader,类型引用>到缓存,这里真实加载类的ClassLoader我们可以叫做defineClassLoader;
  3. 返回的类型在交给Java之前,将会判断defineClassLoader是否等于initialClassLoader,如果不等,则新增<类名,initialClassLoader,类型引用>到缓存。
  4. 这里区分initialClassLoader和defineClassLoader的原因在于,调用initialClassLoader的loadClass,可能最终委派给其他的ClassLoader进行了加载。

6.2 ClassLoader.loadClass(String className)

我们在分析了Class.forName之后,再看ClassLoader.loadClass()就会变得简单很多,这个ClassLoader就是一个指定类加载器,而ClassLoader.loadClass()只是相当于一个简单的方法调用。
根据上图所示,该过程开始于第4步,没有前3步,该过程简单说就是:调用ClassLoader.loadClass(类名),加载到类型后,保存<类名,真实加载类的ClassLoader,类型引用>到缓存,这里真实加载类的ClassLoader我们可以叫做defineClassLoader。也就是,调用ClassLoader.loadClass(类名)之后,并不一定会在缓存中生成一条<类名,ClassLoader,类型引用>的记录,但是一定会生成一条<类名,真实加载类的ClassLoader,类型引用>的记录。(自己附注:实际上最少在JDK8之后loadClass也先到系统字典中查询是否已创建)

6.3 ClassLoader.findLoadedClass(String className)

该方法是protected final修饰的方法,也就是ClassLoader的子类可以内部使用,但是无法通过ClassLoader.findLoadedClass直接调用。
这个方法一直感觉很奇怪,从名称上看就是查询这个ClassLoader加载过的Class,如果加载过了,那么就返回类型实例。

7. 怎么创建一个类加载器

URLClassLoader基本能满足一些个性的类加载需求,如果还不满足,可以实现自己的类加载器。

  1. extend ClassLoader
  2. 覆盖protected Class<?> findClass(String name) throws ClassNotFoundException
    在findClass中实现查找类字节码的逻辑,并调用protected final Class<?> defineClass(String name, byte[] b, int off, int len)得到类。
  3. 注意设置父类加载器。
    默认的父类加载器是系统类加载器,如果设置父类加载器为null,真实的父类加载器是启动类加载器。

上面步骤的类加载器符合双亲制加载规范。Override ClassLoader.loadClass实现了双亲制和缓存细节,不建议打破。

5. Class.getResource(String path)

path不以’/'开头时,默认是从此类所在的包下取资源;
path 以’/'开头时,则是从ClassPath根下获取;
TestMain.class.getResource("/") == t.getClass().getClassLoader().getResource("")

7. 各种错误

遇到类加载器问题时,可以尝试使用下面的表格进行问题排查。

类找不到 加载了不正确的类 多于一个类被加载
ClassNotFoundException NoClassDefFoundError IncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessError ClassCastException LinkageError
IDE class lookup (Ctrl+Shift+T in Eclipse) 或者 find . -name "*.jar" -exec jar -tf {} ; \ grep DateUtils 使用middelware-detector 通过在启动参数中加 -verbose:class,观察加载的类来自哪个jar包 使用middelware-detector

7.1 ClassNotFoundException 和NoClassDefFoundError的区别


sometimes error on static initializer block can also result in NoClassDefFoundError.

7.2 LinkageError

LinkageError 需要观察哪个类被不同的类加载器加载了,在哪个方法或者调用处发生(交汇)的,然后才能想解决方法,解决方法无外乎两种。

  1. 还是不同的类加载器加载,但是相互不再交汇影响,这里需要针对发生问题的地方做一些改动,比如更换实现方式,避免出现上述问题;
  2. 冲突的类需要由一个Parent类加载器进行加载。LinkageError 和ClassCastException 本质是一样的,加载自不同类加载器的类型,在同一个类的方法或者调用中出现,如果有转型操作那么就会抛 ClassCastException ,如果是直接的方法调用处的参数或者返回值解析,那么就会产生 LinkageError 。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容