写点什么

你知道 Java 类是如何被加载的吗?,mybatis 从入门到精通 pdf 百度云

用户头像
极客good
关注
发布于: 刚刚

========================================================================

[](

)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.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 了一个新的 Ins


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


tanceKlass 对象。这里并不是简单的在堆上分配内存,要注意的是 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 衍生出来的实例类型也是不一样的。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
你知道 Java 类是如何被加载的吗?,mybatis从入门到精通pdf百度云