108. 你真的知道 Java 类是如何被加载的吗?(一)

简介: 108. 你真的知道 Java 类是如何被加载的吗?(一)

108. 你真的知道 Java 类是如何被加载的吗?(一)


一:前言

最近给一个非Java方向的朋友讲了下双亲委派模型,朋友让我写篇文章深度研究下JVM的ClassLoader,我确实也好久没写JVM相关的文章了,有点手痒痒,涂了皮炎平也抑制不住。

我在向朋友解释的时候是这么说的:双亲委派模型中,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.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的幕后大佬!更多面试题,欢迎关注公众号 Java面试题精选

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。更多面试题,欢迎关注公众号 Java面试题精选

目录
相关文章
|
6天前
|
存储 Java
Java Scanner类详解与应用
Java Scanner类详解与应用
|
1天前
|
存储 并行计算 Java
Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)
Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)
8 2
|
1天前
|
安全 Java 测试技术
滚雪球学Java(51):掌握Java Date类:用法、弊端与时间戳转换技巧,助你轻松处理日期时间
【6月更文挑战第5天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
14 0
滚雪球学Java(51):掌握Java Date类:用法、弊端与时间戳转换技巧,助你轻松处理日期时间
|
3天前
|
Java 编译器
31. 【Java教程】枚举类
31. 【Java教程】枚举类
8 1
|
3天前
|
小程序 Java
28. 【Java教程】Scanner 类
28. 【Java教程】Scanner 类
18 7
|
3天前
|
Java 索引
26. 【Java教程】 String类
26. 【Java教程】 String类
16 0
|
3天前
|
Java 编译器 C语言
18. 【Java教程】类和对象
18. 【Java教程】类和对象
7 1
|
4天前
|
Java 安全 测试技术
滚雪球学Java(49):如何使用StringBuilder类在Java中高效地处理字符串?
【6月更文挑战第3天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
14 3
滚雪球学Java(49):如何使用StringBuilder类在Java中高效地处理字符串?
|
4天前
|
Java 安全 索引
滚雪球学Java(48):面向对象编程中的StringBuffer类详解
【6月更文挑战第2天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
27 5
滚雪球学Java(48):面向对象编程中的StringBuffer类详解
|
5天前
|
搜索推荐 算法 Java
JAVA中的交换类排序算法详解
JAVA中的交换类排序算法详解
12 1