你知道 Java 類是如何被加載的嗎?

一:前言

最近給一個非Java方向的朋友講了下雙親委派模型,朋友讓我寫篇文章深度研究下JVM的ClassLoader,我確實也很久沒寫JVM相關的文章了,有點手癢癢,塗了皮炎平也抑制不住。java

我在向朋友解釋的時候是這麼說的:雙親委派模型中,ClassLoader在加載類的時候,會先交由它的父ClassLoader加載,只有當父ClassLoader加載失敗的狀況下,纔會嘗試本身去加載。這樣能夠實現部分類的複用,又能夠實現部分類的隔離,由於不一樣ClassLoader加載的類是互相隔離的。數據結構

不過貿然的向別人解釋雙親委派模型是不妥的,若是在不瞭解JVM的類加載機制的狀況下,又如何能很好的理解「不一樣ClassLoader加載的類是互相隔離的」這句話呢?因此爲了理解雙親委派,最好的方式,就是先了解下ClassLoader的加載流程。併發

二:Java 類是如何被加載的

2.1:什麼時候加載類dom

咱們首先要清楚的是,Java類什麼時候會被加載?jvm

《深刻理解Java虛擬機》給出的答案是:ide

1:遇到new、getstatic、putstatic 等指令時。
2:對類進行反射調用的時候。
3:初始化某個類的子類的時候。
4:虛擬機啓動時會先加載設置的程序主類。
5:使用JDK 1.7 的動態語言支持的時候。
其實要我說,最通俗易懂的答案就是:當運行過程當中須要這個類的時候。函數

那麼咱們不妨就從如何加載類開始提及。工具

2.2:怎麼加載類oop

利用ClassLoader加載類很簡單,直接調用ClassLoder的loadClass()方法便可,我相信你們都會,可是仍是要舉個栗子:源碼分析

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        Test.class.getClassLoader().loadClass("com.wangxiandeng.test.Dog");
    }
}

上面這段代碼便實現了讓ClassLoader去加載 「com.wangxiandeng.test.Dog」 這個類,是否是 so easy。可是JDK 提供的 API 只是冰山一角,看似很簡單的一個調用,其實隱藏了很是多的細節,我這我的吧,最喜歡作的就是去揭開 API 的封裝,一探究竟。

2.3:JVM 是怎麼加載類的

JVM 默認用於加載用戶程序的ClassLoader爲AppClassLoader,不過不管是什麼ClassLoader,它的根父類都是java.lang.ClassLoader。在上面那個例子中,loadClass()方法最終會調用到ClassLoader.definClass1()中,這是一個 Native 方法。

static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);

看到 Native 方法莫心慌,不要急,打開OpenJDK源碼,我等繼續蜻蜓點水即是!

definClass1()對應的 JNI 方法爲 Java_java_lang_ClassLoader_defineClass1()

JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_defineClass1(JNIEnv *env,
                                        jclass cls,
                                        jobject loader,
                                        jstring name,
                                        jbyteArray data,
                                        jint offset,
                                        jint length,
                                        jobject pd,
                                        jstring source)
{
    ......
    result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource);
    ......
    return result;
}

Java_java_lang_ClassLoader_defineClass1 主要是調用了JVM_DefineClassWithSource()加載類,跟着源碼往下走,會發現最終調用的是 jvm.cpp 中的 jvm_define_class_common()方法。

static jclass jvm_define_class_common(JNIEnv *env, const char *name,
                                      jobject loader, const jbyte *buf,
                                      jsize len, jobject pd, const char *source,
                                      TRAPS) {
  ......
  ClassFileStream st((u1*)buf, len, source, ClassFileStream::verify);
  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));
  Klass* k = SystemDictionary::resolve_from_stream(class_name,
                                                   class_loader,
                                                   protection_domain,
                                                   &st,
                                                   CHECK_NULL);
  ......

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

上面這段邏輯主要就是利用 ClassFileStream 將要加載的class文件轉成文件流,而後調用SystemDictionary::resolve_from_stream(),生成 Class 在 JVM 中的表明:Klass。對於Klass,你們可能不太熟悉,可是在這裏必須得了解下。說白了,它就是JVM 用來定義一個Java Class 的數據結構。不過Klass只是一個基類,Java Class 真正的數據結構定義在 InstanceKlass中。

class InstanceKlass: public Klass {

 protected:

  Annotations*    _annotations;
  ......
  ConstantPool* _constants;
  ......
  Array<jushort>* _inner_classes;
  ......
  Array<Method*>* _methods;
  Array<Method*>* _default_methods;
  ......
  Array<u2>*      _fields;
}

可見 InstanceKlass 中記錄了一個 Java 類的全部屬性,包括註解、方法、字段、內部類、常量池等信息。這些信息原本被記錄在Class文件中,因此說,InstanceKlass就是一個Java Class 文件被加載到內存後的形式。

再回到上面的類加載流程中,這裏調用了 SystemDictionary::resolve_from_stream(),將 Class 文件加載成內存中的 Klass。

resolve_from_stream() 即是重中之重!主要邏輯有下面幾步:

1:判斷是否容許並行加載類,並根據判斷結果進行加鎖。

bool DoObjectLock = true;
if (is_parallelCapable(class_loader)) {
  DoObjectLock = false;
}
ClassLoaderData* loader_data = register_loader(class_loader, CHECK_NULL);
Handle lockObject = compute_loader_lock_object(class_loader, THREAD);
check_loader_lock_contention(lockObject, THREAD);
ObjectLocker ol(lockObject, THREAD, DoObjectLock);

若是容許並行加載,則不會對ClassLoader進行加鎖,只對SystemDictionary加鎖。不然,便會利用 ObjectLocker 對ClassLoader 加鎖,保證同一個ClassLoader在同一時刻只能加載一個類。ObjectLocker 會在其構造函數中獲取鎖,並在析構函數中釋放鎖。

容許並行加載的好處即是精細化了鎖粒度,這樣能夠在同一時刻加載多個Class文件。

2:解析文件流,生成 InstanceKlass。

InstanceKlass* k = NULL;

k = KlassFactory::create_from_stream(st,
                                         class_name,
                                         loader_data,
                                         protection_domain,
                                         NULL, // host_klass
                                         NULL, // cp_patches
                                         CHECK_NULL);

3:利用SystemDictionary註冊生成的 Klass。

SystemDictionary 是用來幫助保存 ClassLoader 加載過的類信息的。準確點說,SystemDictionary並非一個容器,真正用來保存類信息的容器是 Dictionary,每一個ClassLoaderData 中都保存着一個私有的 Dictionary,而 SystemDictionary 只是一個擁有不少靜態方法的工具類而已。

咱們來看看註冊的代碼:

if (is_parallelCapable(class_loader)) {
  InstanceKlass* defined_k = find_or_define_instance_class(h_name, class_loader, k, THREAD);
  if (!HAS_PENDING_EXCEPTION && defined_k != k) {
    // If a parallel capable class loader already defined this class, register 'k' for cleanup.
    assert(defined_k != NULL, "Should have a klass if there's no exception");
    loader_data->add_to_deallocate_list(k);
    k = defined_k;
  }
} else {
  define_instance_class(k, THREAD);
}

若是容許並行加載,那麼前面就不會對ClassLoader加鎖,因此在同一時刻,可能對同一Class文件加載了屢次。可是同一Class在同一ClassLoader中必須保持惟一性,因此這裏會先利用 SystemDictionary 查詢 ClassLoader 是否已經加載過相同 Class。

若是已經加載過,那麼就將當前線程剛剛加載的InstanceKlass加入待回收列表,並將 InstanceKlass* k 從新指向利用SystemDictionary查詢到的 InstanceKlass。
若是沒有查詢到,那麼就將剛剛加載的 InstanceKlass 註冊到 ClassLoader的 Dictionary 中 中。
雖然並行加載不會鎖住ClassLoader,可是會在註冊 InstanceKlass 時對 SystemDictionary 加鎖,因此不須要擔憂InstanceKlass 在註冊時的併發操做。

若是禁止了並行加載,那麼直接利用SystemDictionary將 InstanceKlass 註冊到 ClassLoader的 Dictionary 中便可。

resolve_from_stream()的主要流程就是上面三步,很明顯,最重要的是第二步,從文件流生成InstanceKlass。

生成InstanceKlass 調用的是 KlassFactory::create_from_stream()方法,它的主要邏輯就是下面這段代碼。

ClassFileParser parser(stream,
                       name,
                       loader_data,
                       protection_domain,
                       host_klass,
                       cp_patches,
                       ClassFileParser::BROADCAST, // publicity level
                       CHECK_NULL);

InstanceKlass* result = parser.create_instance_klass(old_stream != stream, CHECK_NULL);

原來 ClassFileParser 纔是真正的主角啊!它纔是將Class文件昇華成InstanceKlass的幕後大佬!

2.4:不得不說的ClassFileParser

ClassFileParser 加載Class文件的入口即是 create_instance_klass()。顧名思義,用來建立InstanceKlass的。

create_instance_klass()主要就幹了兩件事:

(1):爲 InstanceKlass 分配內存

InstanceKlass* const ik =
    InstanceKlass::allocate_instance_klass(*this, CHECK_NULL);

(2):分析Class文件,填充 InstanceKlass 內存區域

fill_instance_klass(ik, changed_by_loadhook, CHECK_NULL);
咱們先來講道說道第一件事,爲 InstanceKlass 分配內存。

內存分配代碼以下:

const int size = InstanceKlass::size(parser.vtable_size(),
                                       parser.itable_size(),
                                       nonstatic_oop_map_size(parser.total_oop_map_count()),
                                       parser.is_interface(),
                                       parser.is_anonymous(),
                                       should_store_fingerprint(parser.is_anonymous()));
ClassLoaderData* loader_data = parser.loader_data();
InstanceKlass* ik;
ik = new (loader_data, size, THREAD) InstanceKlass(parser, InstanceKlass::_misc_kind_other);

這裏首先計算了InstanceKlass在內存中的大小,要知道,這個大小在Class 文件編譯後就被肯定了。

而後便 new 了一個新的 InstanceKlass 對象。這裏並非簡單的在堆上分配內存,要注意的是Klass 對 new 操做符進行了重載:

void* Klass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw() {
  return Metaspace::allocate(loader_data, word_size, MetaspaceObj::ClassType, THREAD);
}

分配 InstanceKlass 的時候調用了 Metaspace::allocate():

MetaspaceObj::Type type, TRAPS) {
  ......
  MetadataType mdtype = (type == MetaspaceObj::ClassType) ? ClassType : NonClassType;
  ......
  MetaWord* result = loader_data->metaspace_non_null()->allocate(word_size, mdtype);
  ......
  return result;
}

因而可知,InstanceKlass 是分配在 ClassLoader的 Metaspace(元空間) 的方法區中。從 JDK8 開始,HotSpot 就沒有了永久代,類都分配在 Metaspace 中。Metaspace 和永久代不同,採用的是 Native Memory,永久代因爲受限於 MaxPermSize,因此當內存不夠時會內存溢出。

分配完 InstanceKlass 內存後,便要着手第二件事,分析Class文件,填充 InstanceKlass 內存區域。

ClassFileParser 在構造的時候就會開始分析Class文件,因此fill_instance_klass()中只須要填充便可。填充結束後,還會調用 java_lang_Class::create_mirror()建立 InstanceKlass 在Java 層的 Class 對象。

void ClassFileParser::fill_instance_klass(InstanceKlass* ik, bool changed_by_loadhook, TRAPS) {
  .....
  ik->set_class_loader_data(_loader_data);
  ik->set_nonstatic_field_size(_field_info->nonstatic_field_size);
  ik->set_has_nonstatic_fields(_field_info->has_nonstatic_fields);
  ik->set_static_oop_field_count(_fac->count[STATIC_OOP]);
  ik->set_name(_class_name);
  ......

  java_lang_Class::create_mirror(ik,
                                 Handle(THREAD, _loader_data->class_loader()),
                                 module_handle,
                                 _protection_domain,
                                 CHECK);
}

順便提一句,對於Class文件結構不熟悉的同窗,能夠看下我兩年前寫的一篇文章:

《汪先生:Jvm之用java解析class文件》

到這兒,Class文件已經完成了華麗的轉身,由冷冰冰的二進制文件,變成了內存中充滿生命力的InstanceKlass。

三:再談雙親委派

若是你耐心的看完了上面的源碼分析,你必定對 「不一樣ClassLoader加載的類是互相隔離的」 這句話的理解又上了一個臺階。

咱們總結下:每一個ClassLoader都有一個 Dictionary 用來保存它所加載的InstanceKlass信息。而且,每一個 ClassLoader 經過鎖,保證了對於同一個Class,它只會註冊一份 InstanceKlass 到本身的 Dictionary 。

正式因爲上面這些緣由,若是全部的 ClassLoader 都由本身去加載 Class 文件,就會致使對於同一個Class文件,存在多份InstanceKlass,因此即便是同一個Class文件,不一樣InstanceKlasss 衍生出來的實例類型也是不同的。

舉個栗子,咱們自定義一個 ClassLoader,用來打破雙親委派模型:

public class CustomClassloader extends URLClassLoader {

    public CustomClassloader(URL[] urls) {
        super(urls);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (name.startsWith("com.wangxiandeng")) {
            return findClass(name);
        }
        return super.loadClass(name, resolve);
    }
}

再嘗試加載Studen類,並實例化:

public class Test {

    public static void main(String[] args) throws Exception {
        URL url[] = new URL[1];
        url[0] = Thread.currentThread().getContextClassLoader().getResource("");

        CustomClassloader customClassloader = new CustomClassloader(url);
        Class clazz = customClassloader.loadClass("com.wangxiandeng.Student");

        Student student = (Student) clazz.newInstance();
    }
}

運行後便會拋出類型強轉異常:

Exception in thread "main" java.lang.ClassCastException:
      com.wangxiandeng.Student cannot be cast to com.wangxiandeng.Student

爲何呢?

由於實例化的Student對象所屬的 InstanceKlass 是由CustomClassLoader加載生成的,而咱們要強轉的類型Student.Class 對應的 InstanceKlass 是由系統默認的ClassLoader生成的,因此本質上它們就是兩個毫無關聯的InstanceKlass,固然不能強轉。

有同窗問到:爲何「強轉的類型Student.Class 對應的 InstanceKlass 是由系統默認的ClassLoader生成的」?
其實很簡單,咱們反編譯下字節碼:

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=5, args_size=1
         0: iconst_1
         1: anewarray     #2                  // class java/net/URL
         4: astore_1
         5: aload_1
         6: iconst_0
         7: invokestatic  #3                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        10: invokevirtual #4                  // Method java/lang/Thread.getContextClassLoader:()Ljava/lang/ClassLoader;
        13: ldc           #5                  // String
        15: invokevirtual #6                  // Method java/lang/ClassLoader.getResource:(Ljava/lang/String;)Ljava/net/URL;
        18: aastore
        19: new           #7                  // class com/wangxiandeng/classloader/CustomClassloader
        22: dup
        23: aload_1
        24: invokespecial #8                  // Method com/wangxiandeng/classloader/CustomClassloader."<init>":([Ljava/net/URL;)V
        27: astore_2
        28: aload_2
        29: ldc           #9                  // String com.wangxiandeng.Student
        31: invokevirtual #10                 // Method com/wangxiandeng/classloader/CustomClassloader.loadClass:(Ljava/lang/String;)Ljava/lang/Class;
        34: astore_3
        35: aload_3
        36: invokevirtual #11                 // Method java/lang/Class.newInstance:()Ljava/lang/Object;
        39: checkcast     #12                 // class com/wangxiandeng/Student
        42: astore        4
        44: return

能夠看到在利用加載的Class初始化實例後,調用了 checkcast 進行類型轉化,checkcast 後的操做數 #12 即爲Student這個類在常量池中的索引:#12 = Class #52 // com/wangxiandeng/Student
下面咱們能夠看看 checkcast 在HotSpot中的實現。
HotSpot 目前有三種字節碼執行引擎,目前採用的是模板解釋器,能夠看下我這篇文章:《汪先生:JVM之模板解釋器》。
早期的HotSpot採用的是字節碼解釋器。模板解釋器對於指令的執行都是用匯編寫的,而字節碼解釋器採用的C++進行的翻譯,爲了看起來比較舒服,咱們就不看彙編了,直接看字節碼解釋器就好了。若是你的彙編功底很好,固然也能夠直接看模板解釋器,我以前寫的文章《汪先生:JVM之建立對象源碼分析》這裏就是分析模板解釋器對於 new 指令的實現。

廢話很少說,咱們來看看字節碼解釋器對於checkcast的實現,代碼在 bytecodeInterpreter.cpp 中

CASE(_checkcast):
    if (STACK_OBJECT(-1) != NULL) {
      VERIFY_OOP(STACK_OBJECT(-1));
      // 拿到 checkcast 指令後的操做數,本例子中即 Student.Class 在常量池中的索引:#12
      u2 index = Bytes::get_Java_u2(pc+1);

      // 若是常量池尚未解析,先進行解析,即將常量池中的符號引用替換成直接引用,
      //此時就會觸發Student.Class 的加載
      if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
        CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
      }
      // 獲取上一步系統加載的Student.Class 對應的 InstanceKlass
      Klass* klassOf = (Klass*) METHOD->constants()->resolved_klass_at(index);
      // 獲取要強轉的對象的實際類型,即咱們本身手動加載的Student.Class 對應的 InstanceKlass
      Klass* objKlass = STACK_OBJECT(-1)->klass(); // ebx

      // 如今就比較簡單了,直接看看上面的兩個InstanceKlass指針內容是否相同
      // 不一樣的狀況下則判斷是否存在繼承關係
      if (objKlass != klassOf && !objKlass->is_subtype_of(klassOf)) {
        // Decrement counter at checkcast.
        BI_PROFILE_SUBTYPECHECK_FAILED(objKlass);
        ResourceMark rm(THREAD);
        char* message = SharedRuntime::generate_class_cast_message(
          objKlass, klassOf);
        VM_JAVA_ERROR(vmSymbols::java_lang_ClassCastException(), message, note_classCheck_trap);
      }
      // Profile checkcast with null_seen and receiver.
      BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/false, objKlass);
    } else {
      // Profile checkcast with null_seen and receiver.
      BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/true, NULL);
    }

經過對上面代碼的分析,我相信你們已經理解了 「強轉的類型Student.Class 對應的 InstanceKlass 是由系統默認的ClassLoader生成的」 這句話了。

雙親委派的好處是儘可能保證了同一個Class文件只會生成一個InstanceKlass,可是某些狀況,咱們就不得不去打破雙親委派了,好比咱們想實現Class隔離的時候。

回覆下簫陌同窗的問題:

// 若是常量池尚未解析,先進行解析,即將常量池中的符號引用替換成直接引用,
//此時就會觸發Student.Class 的加載
if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
}
請問,爲什麼這裏會從新加載Student.Class?jvm是否是有本身的class加載鏈路,而後系統循着鏈路去查找class是否已經被加載?那該怎麼把自定義的CustomClassloader 加到這個查詢鏈路中去呢?

第一種方法:設置啓動參數 java -Djava.system.class.loader

第二種方法:利用Thread.setContextClassLoder

這裏就有點技巧了,看下代碼:

public class Test {

    public static void main(String[] args) throws Exception {
        URL url[] = new URL[1];
        url[0] = Thread.currentThread().getContextClassLoader().getResource("");
        final CustomClassloader customClassloader = new CustomClassloader(url);
        Thread.currentThread().setContextClassLoader(customClassloader);
        Class clazz = customClassloader.loadClass("com.wangxiandeng.ClassTest");
        Object object = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("test");
        method.invoke(object);
    }
}
public class ClassTest {

    public void test() throws Exception{
        Class clazz = Thread.currentThread().getContextClassLoader().loadClass("com.wangxiandeng.Student");
        Student student = (Student) clazz.newInstance();
        System.out.print(student.getClass().getClassLoader());

    }
}

要注意的是在設置線程的ClassLoader後,並非直接調用 new ClassTest().test()。爲何呢?由於直接強引用的話,會在解析Test.Class的常量池時,利用系統默認的ClassLoader加載了ClassTest,從而又觸發了ClassTest.Class的解析。爲了不這種狀況的發生,這裏利用CustomClassLoader去加載ClassTest.Class,再利用反射機制調用test(),此時在解析ClassTest.Class的常量池時,就會利用CustomClassLoader去加載Class常量池項,也就不會發生異常了。

四:總結

寫完這篇文章,手也不癢了,甚爽!這篇文章從雙親委派講到了Class文件的加載,最後又繞回到雙親委派,看似有點繞,其實只有理解了Class的加載機制,才能更好的理解相似雙親委派這樣的機制,不然只死記硬背一些空洞的理論,是沒法起到由內而外的理解的。



本文做者:中間件小哥

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索