JVM类加载器-源码分析

前言

我们在JVM类加载器-原理一文中了解了JVM类加载器的基本原理。现在我们一起通过ClassLoader类及其相关源码来详细分析、理解JVM类加载器的体系,深入理解JVM类加载器的原理与实现。

ClassLoader的主要责任就是加载类。ClassLoader通过一个类的名字,定位并且把这个class文件加载进JVM的内存里,生成可以表示这个类的结构。ClassLoader是JDK为我们提供的一个基础的类加载器,它本身是一个抽象类,我们在实现自己特殊需求的类加载器的时候,只需要根据我们自己的需要,覆写findClass方法(通过类的全限定名查找该类的class文件)。

ClassLoader的创建

ClassLoader的构造函数是private的,所以不能直接new一个ClassLoader实例。而是在ClassLoader中提供了一些静态方法,产生特定ClassLoader。如:

//该方法返回系统类加载器,该类加载器也是典型的用来启动应用的类加载器。
//该方法在运行时的启动序列里被首次调用,在这个时间点上,该方法构造的类加载器被设置为调用线程的上下文类加载器。
@CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }
private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
//判断类加载器是否支持并发
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();//并发情况下,每个线程通过同一个ClassLoader实例进行类的加载时,都会获得各自的锁
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
  • 类加载的入口是从loadClass方法开始的,而类加载器的双亲委派模型也是在这里实现的。源码如下:
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    /**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     * 该方法通过具体的类的名字加载这个类。默认的实现按以下的顺序搜寻这个类
     * <p><ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     * 调用findLoadedClass(String)方法检查该类是否已经被加载过
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     * 调用父类加载器的loadClass(String)方法来加载这个类,如果父类加载器为null则使用虚拟机内置的类加载器。
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     * 
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     * 官方建议继承ClassLoader的子类去覆写findClass方法而非loadClass方法
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     * 除非被覆写,该方法会在类加载的整个过程中持有getClassLoadingLock方法返回的锁
     */
    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;
        }
    }

具体的逻辑分析如下:

  • 在加载类期间,调用线程会一直持有一把锁。基于loadClass方法的逻辑,我们可以很容易理解为什么loadClass方法的主要逻辑需要在一个同步块里。后续的逻辑里有很多对ClassLoader共享变量的操作如parent的赋值等。
/**
     * Returns the lock object for class loading operations.
     * For backward compatibility, the default implementation of this method
     * behaves as follows. If this ClassLoader object is registered as
     * parallel capable, the method returns a dedicated object associated
     * with the specified class name. Otherwise, the method returns this
     * ClassLoader object. </p>
     * 为了向前兼容,该方法实现如下:如果ClassLoader对象被注册为支持并发,那么该方法会new一个Object作为锁返回;否则,同步块的锁就是ClassLoader本身;
     * @since  1.7
     */
    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);//parallelLockMap是一个ConcurrentHashMap
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
  • 检查类是否已经加载。
 protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }
//简单的检查加载的类的名字是否为空或是无效
private boolean checkName(String name) {
        if ((name == null) || (name.length() == 0))
            return true;
        if ((name.indexOf('/') != -1)
            || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
            return false;
        return true;
    }
private native final Class findLoadedClass0(String name);

我们发现findLoadedClass0是一个native方法。查了下openjdk源码如下:

JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_findLoadedClass0(JNIEnv *env, jobject loader,
                                           jstring name)
{
    if (name == NULL) {
        return 0;
    } else {
        return JVM_FindLoadedClass(env, loader, name);
    }
JVM_ENTRY(jclass, JVM_FindLoadedClass(JNIEnv *env, jobject loader, jstring name))
  JVMWrapper("JVM_FindLoadedClass");
  ResourceMark rm(THREAD);

  Handle h_name (THREAD, JNIHandles::resolve_non_null(name));
  Handle string = java_lang_String::internalize_classname(h_name, CHECK_NULL);

  const char* str   = java_lang_String::as_utf8_string(string());
  // Sanity check, don't expect null
  if (str == NULL) return NULL;

  const int str_len = (int)strlen(str);
  if (str_len > Symbol::max_length()) {
    //类名长度有限制
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    return NULL;
  }
  //
  TempNewSymbol klass_name = SymbolTable::new_symbol(str, str_len, CHECK_NULL);

  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  Handle h_loader(THREAD, JNIHandles::resolve(loader));
  if (UsePerfData) {
    is_lock_held_by_thread(h_loader,
                           ClassLoader::sync_JVMFindLoadedClassLockFreeCounter(),
                           THREAD);
  }
  //全局符号字典查找该类的符号是否存在
  klassOop k = SystemDictionary::find_instance_or_array_klass(klass_name,
                                                              h_loader,
                                                              Handle(),
                                                              CHECK_NULL);

  return (k == NULL) ? NULL :
            (jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror());
JVM_END
}

HotSpot虚拟机在永久代中增加了符号表。该表为哈希表用于将直接引用与运行时常量池的符号引用作射。所以该native方法的实质就是在jvm的常量池中查找该类的符号引用是否存在。

  • 接着的逻辑就是类的双亲加载机制的实现。把类加载的任务交给父类加载器执行,直到父类加载器为空,此时会返回通过JDK提供的系统启动类加载器加载的类。
private Class findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

    // return null if not found
    private native Class findBootstrapClass(String name);
  • 如果最后发现该类没有被加载过,则由当前类加载器调用findClass方法来继续加载该类。

findClass

该方法和它的名字一样,就是根据类的名字ClassLoader不提供该方法的具体实现,要求我们根据自己的需要来覆写该方法。
所以我们可以看一看URLClassLoader对findClass方法的实现,类加载的工作又被代理给了defineClass方法:

protected Class<?> More ...findClass(final String name)
351         throws ClassNotFoundException
352    {
353        try {
354            return AccessController.doPrivileged(
355                new PrivilegedExceptionAction<Class>() {
356                    public Class More ...run() throws ClassNotFoundException {
357                        String path = name.replace('.', '/').concat(".class");
358                        Resource res = ucp.getResource(path, false);
359                        if (res != null) {
360                            try {
361                                return defineClass(name, res);
362                            } catch (IOException e) {
363                                throw new ClassNotFoundException(name, e);
364                            }
365                        } else {
366                            throw new ClassNotFoundException(name);
367                        }
368                    }
369                }, acc);
370        } catch (java.security.PrivilegedActionException pae) {
371            throw (ClassNotFoundException) pae.getException();
372        }
373    }

defineClass

defineClass方法主要是把字节数组转化为类的实例。同时definClass方法为final的,故不可以覆写。
同时defineClass也是一个native方法,具体也是由虚拟机实现,源码如下:

// common code for JVM_DefineClass() and JVM_DefineClassWithSource()
// and JVM_DefineClassWithSourceCond()
static jclass jvm_define_class_common(JNIEnv *env, const char *name,
                                      jobject loader, const jbyte *buf,
                                      jsize len, jobject pd, const char *source,
                                      jboolean verify, TRAPS) {
  if (source == NULL)  source = "__JVM_DefineClass__";

  assert(THREAD->is_Java_thread(), "must be a JavaThread");
  JavaThread* jt = (JavaThread*) THREAD;

  PerfClassTraceTime vmtimer(ClassLoader::perf_define_appclass_time(),
                             ClassLoader::perf_define_appclass_selftime(),
                             ClassLoader::perf_define_appclasses(),
                             jt->get_thread_stat()->perf_recursion_counts_addr(),
                             jt->get_thread_stat()->perf_timers_addr(),
                             PerfClassTraceTime::DEFINE_CLASS);

  if (UsePerfData) {
    ClassLoader::perf_app_classfile_bytes_read()->inc(len);
  }

  // Since exceptions can be thrown, class initialization can take place
  // if name is NULL no check for class name in .class stream has to be made.
  TempNewSymbol class_name = NULL;
  if (name != NULL) {
    const int str_len = (int)strlen(name);
    if (str_len > Symbol::max_length()) {
      // It's impossible to create this class;  the name cannot fit
      // into the constant pool.
      THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
    }
    //为类创建符号
    class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL);
  }

  ResourceMark rm(THREAD);
  ClassFileStream st((u1*) buf, len, (char *)source);
  Handle class_loader (THREAD, JNIHandles::resolve(loader));
  if (UsePerfData) {
    is_lock_held_by_thread(class_loader,
                           ClassLoader::sync_JVMDefineClassLockFreeCounter(),
                           THREAD);
  }
  Handle protection_domain (THREAD, JNIHandles::resolve(pd));
  //从字节文件中为该类解析创建一个klassOop对象,表示Java类
  klassOop k = SystemDictionary::resolve_from_stream(class_name, class_loader,
                                                     protection_domain, &st,
                                                     verify != 0,
                                                     CHECK_NULL);

  if (TraceClassResolution && k != NULL) {
    trace_class_resolution(k);
  }

  return (jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror());
}

AppClassLoader/ExtClassLoader/BootstrapClassLoader

系统为我们提供的几个类记载器主要是在sun.misc.Launcher类找那个定义。sun.misc.Launcher$AppClassLoader主要用于加载java.class.path目录下的类,sun.misc.Launcher$ExtClassLoader主要用于加载<JAVA_HOME>\lib\ext或java.ext.dirs指定路径下的类库。这些类的具体继承关系如下:


classloaderext.png

启动类加载器是无法被Java程序直接引用,它是在虚拟机启动时创建的,需要它来加载类时,都是通过navtive方法findBootstrapClass来简洁使用的。

总结

类加载器的双亲委派模型的实现逻辑十分简单,都在ClassLoader的loadClass方法中,我们在实现自己的类加载器时,最好遵守这种模型,由于在jvm中,一个类类型实例的唯一性是由加载他的类加载器与这个类的类型共同确定的。这对保证Java程序的稳定运作十分重要。

参考引用

  • JDK1.7源码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容