前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类加载与 Java主类加载机制解析

类加载与 Java主类加载机制解析

作者头像
博文视点Broadview
发布2020-06-11 14:28:19
8280
发布2020-06-11 14:28:19
举报

小编说:类的加载机制与生命周期等概念,在各种书籍与各种网络博客里随处可见,然而对于一个想要真正了解其内部实现的人而言,那些都涉入过浅。本文从JVM源码的角度,还原出Java类加载的真实机制。 本文选自《揭秘Java虚拟机:JVM设计原理与实现》

  • 类加载——镜像类与静态字段

类加载的最终结果便是在JVM的方法区创建一个与Java类对等的instanceKlass实例对象,但是在JVM创建完instanceKlass之后,又创建了与之对等的另一个镜像类——java.lang.Class。在JDK 6中,创建镜像类的逻辑被包含在instanceKlassKlass::allocate_instance_klass()函数中,在该函数的末尾执行 java_lang_Class::create_mirror()调用,该接口实现逻辑如下:

通过观察这段源码可知,所谓的mirror镜像类,其实也是instanceKlass的一个实例对象,SystemDictionary::Class_klass()返回的便是java_lang_Class类型,因此instanceMirrorKlass:: cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0)这行代码就是用来创建java.lang.Class这个Java类型在JVM内部对等的instanceKlass实例的。接着通过k->set_java_ mirror(mirror())调用,让当前所创建的klassOop引用刚刚实例化的java.lang.Class对象。JVM之所以在instanceKlass之外再创建一个mirror,是有用意的,总体而言,java.lang.Class是为了被Java程序调用,而instanceKlass则是为了被JVM内部访问。所以,JVM直接暴露给Java的是 java_mirror, 而不是 InstanceKlass。

事实上,JDK类库中所提供的反射等工具类,其实都基于java.lang.Class这个内部镜像实现。例如下面这个Java程序:

该示例Java类很简单,Test类中包含2个公开的字段和一个公开的方法,在main()方法中通过java.lang.Class.for(String)接口反射获取Test类型,反射之后通过java.lang.Class.getFields()接口获取Test类中所包含的全部公开字段数组,并遍历字段数组,打印出字段名。运行该程序,输出如下:

打印结果显示Test类中一共包含2个公开字段,与定义的完全一致。在这里,重点研究的是,java.lang.Class.getFields()接口究竟如何知道Test类中有两个公开的字段。源码面前无秘密。首先看java.lang.Class.getFields()接口,该接口最终会调用java.lang.Class.getDeclaredFields0 (boolean publicOnly)接口,该接口是一个native接口,其最终调用的接口位于HotSpot内部的函数中,该函数如下:

上面这个JVM_GetClassDeclaredFields()函数便是java.lang.Class.getDeclaredFields0 (boolean publicOnly)这个Java类方法所对应的内部实现。由于java.lang.Class.getDeclaredFields0 (boolean publicOnly)方法是类的成员方法,因此该方法包含一个隐藏的入参this,this指向java.lang.Class类型实例自己,所以调用的JVM_GetClassDeclaredFields()函数的第2个入参ofClass便是java.lang.Class类型实例。同时,在执行上面这个JVM_GetClassDeclaredFields()函数调用时,说明其前面的一个步骤——Class klass = Class.forName(“Test”)已经执行完了,此时在JVM内部的klass实例,实际上是Test类型在JVM内部的镜像类,虽然java.lang.Class仅仅是一个镜像类,但是也保存了Test这个Java类中的全部信息,所以在JVM_GetClassDeclaredFields()函数中能够获取Test类中的全部字段。这便是Java反射的原理。通过本示例也可以知道,Java的反射是离不开java.lang.Class这个镜像类的。

如果思维再放得开阔一点,可以这样认为,即使JVM内部没有安排java.lang.Class这么一个媒介作为面向对象反射的基础,那么JVM也必然要定义另外类,假设这个类就叫作Reflection,这个类能够直接被Java程序开发者使用,那么Reflection这个类也必然需要在JVM内部与所要反射的目标Java类所对应的instanceKlass之间建立联系,能够让Java开发者通过这个Reflection类反射出目标Java类的字段、方法等全部信息。从这个意义上而言,java.lang.Class并非是偶然有的,而是必然,是Java这种面向对象的语言与虚拟机实现机制这两种规范下的必然技术实现,如果非要说有巧合的话,那便是恰好叫了“java.lang.Class”这个类名。

既然java.lang.Class是一个必然的存在,所以每次JVM在内部为Java类创建一个对等的instanceKlass时,都要再创建一个对应的Class镜像类,作为反射的基础。

刚才讲过,在JDK 6中,静态字段会存储在instanceKlass的预留空间里,在JVM为instanceKlass申请内存空间时已经为静态字段预留了空间,而在创建完instanceKlass之后,JVM在ClassFileParser::parseClassFile()函数中调用this_klass->do_local_static_fields(&initialize_ static_field, CHECK_(nullHandle))对这部分内存空间进行初始化,do_local_static_fields()函数的实现如下:

这段逻辑遍历Java类中的全部静态字段并逐个将其塞进instanceKlass的预留空间中。在这段逻辑中,需要注意,instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)函数的第一个入参是函数指针,看上面这段逻辑,instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)内部调用了instanceKlass::do_local_static_fields_ impl(instanceKlassHandle this_oop, void f(fieldDescriptor* fd, TRAPS), TRAPS),而在后者内部则通过函数指针f调用其指向的函数。那么指针f指向哪个函数呢?

在ClassFileParser::parseClassFile()函数中调用instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)时,所传入的函数指针是&initialize_static_field,所以该指针指向的函数如下:

在该函数中,通过调用h_k()->**_field_put()系列接口,将不同类型的静态字段存储到instanceKlass对象实例的预留内存空间中,如此便完成了Java类中静态字段的存储。而在JDK 8中,静态字段不再存储于instanceKlass预留空间,而是转移到instanceKlass的镜像类——java. lang.Class的预留空间里去,因此在JDK 8的源码中,上面的这个initialize_static_field()函数定义到javaClasses.cpp中了。同时,创建mirror镜像类的接口也不再在java_lang_Class::create_mirror()函数中调用,而是在ClassFileParser::parseClassFile()函数中调用。虽然调用的地方不同了,但是函数实现的内部机制并没有从根本上发生变化,因此从这一点上看,JDK 6和JDK 8并没有做很大的变更。JDK 8之所以要将静态字段从instanceKlass迁移到mirror中,也不是没有道理,毕竟静态字段并非Java类的成员变量,如果从数据结构这个角度看,静态字段不能算作Java类这个数据结构的一部分,因此JDK 8将静态字段转移到mirror中。从反射的角度看,静态字段放在mirror中是合理的,毕竟在进行反射时,需要给出Java类中所定义的全部字段,无论字段是不是静态类型。例如,将上面的Test类做个修改,在里面增加一个static类型的公开字段,则最终的打印结果会包含该字段。

综上所述,对于JDK 6而言,类加载阶段所产出的最终结果便是如下图所示的这两个实例对象。

java类加载阶段所产生的结果

在JDK 6中,由于mirror也是一个instanceKlass,因此其包含了instanceKlass所包含的一切字段。

  • Java主类加载机制

到上一节为止,Java类加载的过程终于全部讲完了。在前面章节详细讲解了常量池解析、字段解析、方法解析、instanceKlass创建及镜像类的创建。之所以要逐个详细讲解,一方面是因为JVM使用C/C++编写而成,而C/C++语言本身就比Java语言更具难度,相信只要不是直接从事JVM开发的道友,阅读起来都会比较吃力,里面有太多的内存分配、回收、指针、类型转换的内容,笔者作为Java开发者,阅读过程中也费了无数脑筋,相当不轻松,因此笔者感同身受,将一些比较关键的源代码和算法详细描述出来,这是自己辛苦阅读的一种沉淀,相信也会帮助很多对C/C++语言不够熟悉的道友。另一方面是因为JVM作为虚拟机,里面涉及的计算机基础知识多而杂,几乎覆盖了方方面面,其实现也复杂,然而其过程也精彩,所以虽然阅读的过程痛苦,但是结果却是快乐的,理解了原理之后再次面对Java程序,会有一种“一览众山小”之快感,你就是JVM世界里的神,做神的感觉,其美妙不足为外人道也,而这种享受也是支持笔者这两年里一直坚持写下去的最大动力。有苦有乐,生活才能丰富多彩。

牛皮吹完,我们应该总结一下类加载的整体过程了。虚拟机在得到一个Java class 文件流之后,接下来要完成的主要步骤如下:

(1)读取魔数与版本号。

(2)解析常量池,parse_constant_pool()。

(3)解析字段信息,parse_fields()。

(4)解析方法,parse_methods()。

(5)创建与Java类对等的内部对象instanceKlass,new_instanceKlass()。

(6)创建Java镜像类,create_mirror()。

以上便是一个Java类加载的核心流程。了解了类加载的核心流程之后,也许聪明的你会忍不住想,Java类的加载到底何时才会被触发呢?Java类加载的触发条件比较多,其中比较特殊的便是Java程序中包含main()主函数的类——这种类一般也被称作Java程序的主类。Java主类的加载由JVM自动触发——JVM执行完自身的若干初始化逻辑之后,第一个加载的便是Java程序的主类。总体上而言,Java主类加载的链路如下:

上面是Java程序main主类加载的整体链路,该调用链路的核心逻辑如下:

(1)JVM启动后,操作系统会调用java.c::main()主函数,从而进入JVM的世界。java.c::main()方法调用java.c::JavaMain()方法,java.c::JavaMain()方法主要执行JVM的初始化逻辑,初始化完毕之后,便会搜索Java程序的main()主函数所在的类,也即“主类”,找到主类的类名之后,便会调用mainClass = LoadClass(env, classname)对主类进行加载。

(2)LoadClass(env, classname)方法是java.c::LoadClass()方法,而后者执行cls = (*env)->FindClass(env, buf)来寻找主类。

(3)(*env)->FindClass(env, buf)函数首先跳转到jni.cpp::JNI_ENTRY(jclass, jni_ FindClass(JNIEnv *env, const char *name)),JNI_ENTRY是一个宏,在预编译阶段便已展开,这个宏作用的结果是:(*env)->FindClass(env, buf)最终会调用jni.cpp::jni_FindClass(JNIEnv *env, const char *name)函数。

jni.cpp::jni_FindClass(JNIEnv *env, const char *name)函数先调用loader = Handle(THREAD, SystemDictionary::java_system_loader())获取类加载器。Java程序主类的类加载器默认是系统加载器,该加载器是JDK类库中定义的sun.misc.AppClassLoader,关于该加载器的细节会在后文详述。JVM体系中加载器的继承关系如下图所示。

由图上图可知,系统加载器所继承的顶级父类是java.lang.ClassLoader,这是JDK类库所提供的核心加载器。事实上,无论Java程序内部有没有自定义类加载器,最终都会调用java.lang.ClassLoader所提供的几个native接口完成类的加载,这些接口主要包括如下3种:

Java主类的加载也无法绕过这3个接口。

jni.cpp::JNI_ENTRY(jclass, jni_FindClass(JNIEnv *env, const char *name))函数内部获取到系统加载器之后,接着便开始调用find_class_from_class_loader()接口加载主类,而后者则调用SystemDictionary::resolve_or_fail()接口。

(4)SystemDictionary::resolve_or_fail()接口经过一系列调用,最终调用SystemDictionary:: resolve_instance_class_or_null()接口,该接口内部逻辑比较冗长,会经过层层判断,确认同一个加载器没有别的线程在加载同一个类,则最终会执行真正的加载,调用SystemDictionary::load_instance_class()接口,该接口内部执行如下调用:

JavaCalls::call_virtual()接口的主要功能是根据输入的参数,调用指定的Java类中的指定方法。该接口的第2个入参(入参从位置1开始计数)指明所调用的Java类对应的instance,第4个入参指明所调用的特定方法,第5个入参指明所调用的Java类的签名信息。当JVM执行Java程序主类加载时,向JavaCalls::call_virtual()接口传入的第2和第4个入参分别是class_loader和vmSymbols::loadClass_name(),vmSymbols::loadClass_name()返回的方法名是loadClass(),而class_loader则是前置流程中实例化好的系统加载器——AppClassLoader,在JVM内部对等的实例对象。同时,JavaCalls::call_virtual()接口的第5个入参是vmSymbols::string_class_signature(),其返回的字符串是(Ljava/lang/String;)Ljava/lang/Class,该字符串表示所调用的Java方法的入参是Ljava/lang/String,而返回值则是Ljava/lang/Class。由此可知,当JVM加载Java程序的主类时,最终会调用AppClassLoader.loadClass(String)这个方法。由此,JVM的流程便转移到了Java的世界,进入到了Java类的逻辑流之中。

JavaCalls::call_virtual()接口的第6个入参则包含所调用的Java方法所需要的全部入参信息,在JVM加载Java应用程序主类时,向JavaCalls::call_virtual()接口所传入的第6个入参是string,在SystemDictionary::load_instance_class()函数中,该入参封装了所需要加载的Java类的全限定名称,最终这个全限定名称将作为java.lang.AppClassLoader.loadClass(String)接口的入参,系统加载器据此加载目标Java类。

JavaCalls::call_virtual()接口最终会调用JavaCalls::call()接口,JavaCalls::call()接口调用JavaCalls::call_helper(),而后者则会调用StubRoutines::call_stub()例程,对于该例程,阅读过全书的小伙伴一定不会陌生,该例程在本书前面专门花了一章去讲解,有不清楚的小伙伴可以回过去仔细阅读。总体而言,该例程在运行期对应着一段机器码,其作用是辅佐JVM执行Java类方法。这里不得不提一句,JVM作为一款虚拟机,其本身由C/C++语言写成,但是JVM是为执行Java字节码文件而生的,因此JVM内部必然有一套机制能够从C/C++程序调用Java类中的方法,这套机制便通过JavaCalls类来实现,该类中定义了各种call_*()接口,这些接口最终都要调用StubRoutines::call_stub()例程,从而辅佐JVM执行Java方法。

事实上,JavaCalls::call_virtual()接口在JVM内部是一个很常用的接口,大凡涉及Java类成员方法的调用,最终都会经过该接口。

(5)经过上一个步骤,JVM最终会调用sun.misc.AppClassLoader.loadClass(String)接口加载Java应用程序的主类。AppClassLoader继承自java.lang.ClassLoader这个基类,java.lang. ClassLoader.loadClass(String)方法调用loadClass(String, boolean)方法,由于继承的关系,实际调用的是sun.misc.AppClassLoader.loadClass(String, boolean)方法,该方法的实现逻辑如下:

这段代码逻辑是,先判断所加载的类名中是否包含点号“.”,如果包含则说明传入的一定是类的全限定名,包含了包名,则JVM调用SecurityManager模块检查包的访问权限。通过访问权限验证之后,则调用super.loadClass(name, resolve)方法。由于继承关系,super.loadClass(name, resolve)方法其实调用的是java.lang.ClassLoader.loadClass(String name, boolean resolve)方法,该方法的主要逻辑如下:

在java.lang.ClassLoader.loadClass(String name, boolean resolve)方法中,首先通过findLoadedClass(name)方法判断当前加载器是否加载过指定的类,如果没有加载,则判断当前加载器的parent是否为null,如果不为null,则调用parent.loadClass(name, false)方法,通过父加载器加载指定的Java类。AppClassLoader的父加载器是ExtClassLoader,这是扩展类加载器,用于加载JDK中指定路径下的扩展类,这种加载器不会加载Java应用程序的主类,所以程序流会进入if(this.parent != null){}代码块,但是parent.loadClass(name, false)返回null。接着java.lang.ClassLoader.loadClass(String name, boolean resolve)方法只能通过调用this. findClass(name)来加载Java主类。

java.lang.ClassLoader.findClass(String)方法直接抛出异常,因此该类注定要由子类来实现。对于系统类加载器AppClassLoader,其继承自URLClassLoader,因此java.lang.ClassLoader. findClass(String)方法实际指向java.net.URLClassLoader.findClass(String)。java.net.URLClassLoader. findClass(String)方法最终调用java.lang.ClassLoader.defineClass1()这一native接口,这是一个本地接口,由本地类库实现。openjdk项目包含了JDK核心Java类库中的全部本地实现,java.lang. ClassLoader.defineClass1()所对应的本地实现是ClassLoader.c::Java_java_lang_ClassLoader_ defineClass1(),有兴趣的道友可自行查看下其实现,这里就不贴代码了,以免占用过多篇幅。通过调用java.lang.ClassLoader.defineClass1()接口,Java程序流又转移到JVM内部,因此Java类的加载最终仍然是通过JVM本地类库得以实现。

ClassLoader.c::Java_java_lang_ClassLoader_defineClass1()调用jvm.cpp::JVM_DefineClass WithSource(),jvm.cpp::JVM_DefineClassWithSource()调用jvm.cpp::jvm_define_class_common(),而后者则调用SystemDictionary.cpp::resolve_from_stream()接口来加载Java主类。在SystemDictionary.cpp::resolve_from_stream()接口中,终于开始调用ClassFileParser.cpp:: parseClassFile()这个函数来解析Java主类,并最终创建Java主类在JVM内部的对等体——klassInstance,由此完成Java主类的加载。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 博文视点Broadview 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档