关于后端:Java-类是如何被加载的

40次阅读

共计 13219 个字符,预计需要花费 34 分钟才能阅读完成。

生存如果没有指标,就会变得懒惰。一旦决定“明天这样做”,生存一下子就会张弛有度。

一:前言

最近钻研了下双亲委派模型,而后又深度钻研下 JVM 的 ClassLoader。

双亲委派模型中,ClassLoader 在加载类的时候,会先交由它的父 ClassLoader 加载,只有当父 ClassLoader 加载失败的状况下,才会尝试本人去加载。这样能够实现局部类的复用,又能够实现局部类的隔离,因为不同 ClassLoader 加载的类是相互隔离的。

不过贸然的向他人解释双亲委派模型是不妥的,如果在不理解 JVM 的类加载机制的状况下,又如何能很好的了解“不同 ClassLoader 加载的类是相互隔离的”这句话呢?所以为了了解双亲委派,最好的形式,就是先理解下 ClassLoader 的加载流程。

二:Java 类是如何被加载的

2.1:何时加载类

咱们首先要分明的是,Java 类何时会被加载?

《深刻了解 Java 虚拟机》给出的答案是:

  • 遇到 new、getstatic、putstatic 等指令时。
  • 对类进行反射调用的时候。
  • 初始化某个类的子类的时候。
  • 虚拟机启动时会先加载设置的程序主类。
  • 应用 JDK 1.7 的动静语言反对的时候。

其实要我说,最通俗易懂的答案就是:当运行过程中须要这个类的时候。

那么咱们无妨就从如何加载类开始说起。

2.2:怎么加载类

利用 ClassLoader 加载类很简略,间接调用 ClassLoder 的 loadClass() 办法即可,我置信大家都会,然而还是要举个栗子:

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

下面这段代码便实现了让 ClassLoader 去加载“com.malf.test.Dog”这个类,是不是 so easy。然而 JDK 提供的 API 只是冰山一角,看似很简略的一个调用,其实暗藏了十分多的细节,那就让咱们揭开 API 的封装,一探到底。

2.3:JVM 是怎么加载类的

JVM 默认用于加载用户程序的 ClassLoader 为 AppClassLoader,不过无论是什么 ClassLoader,它的根父类都是 java.lang.ClassLoader。在下面那个例子中,loadClass() 办法最终会调用到 ClassLoader.definClass1() 中,这是一个 Native 办法。

private native Class<?> defineClass1(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 文件曾经实现了富丽的转身,由凉飕飕的二进制文件,变成了内存中充斥生命力的 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.malf")) {return findClass(name);
        }
        return super.loadClass(name, resolve);
    }
}

再尝试加载 Student 类,并实例化:

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.malf.Student");
        Student student = (Student) clazz.newInstance();}
}

运行后便会抛出类型强转异样:

Exception in thread "main" java.lang.ClassCastException:  
 com.malf.Student cannot be cast to com.malf.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/malf/Student

上面咱们能够看看 checkcast 在 HotSpot 中的实现。

HotSpot 目前有三种字节码执行引擎,目前采纳的是模板解释器,晚期的 HotSpot 采纳的是字节码解释器。模板解释器对于指令的执行都是用汇编写的,而字节码解释器采纳的 C ++ 进行的翻译,为了看起来比拟难受,咱们就不看汇编了,间接看字节码解释器就行了。如果你的汇编功底很好,当然也能够间接看模板解释器。

废话不多说,咱们来看看字节码解释器对于 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.malf.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.malf.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 的加载机制,能力更好的了解相似双亲委派这样的机制,否则只死记硬背一些空洞的实践,是无奈起到由内而外的了解的。

正文完
 0