JVM類載入器-原始碼分析

weixin_33724059發表於2017-04-30

前言

我們在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指定路徑下的類庫。這些類的具體繼承關係如下:


1593035-c66cbd6fc8109734.png
classloaderext.png

啟動類載入器是無法被Java程式直接引用,它是在虛擬機器啟動時建立的,需要它來載入類時,都是通過navtive方法findBootstrapClass來簡潔使用的。

總結

類載入器的雙親委派模型的實現邏輯十分簡單,都在ClassLoader的loadClass方法中,我們在實現自己的類載入器時,最好遵守這種模型,由於在jvm中,一個類型別例項的唯一性是由載入他的類載入器與這個類的型別共同確定的。這對保證Java程式的穩定運作十分重要。

參考引用

  • JDK1.7原始碼

相關文章