JVM 源码分析05

JVM源码分析之Java类的加载过程

背景

最近对Java细节的底层实现比较感兴趣,比如Java类文件是如何加载到虚拟机的,类对象和方法是以什么数据结构存在于虚拟机中?虚方法、实例方法和静态方法是如何调用的?本文基于openjdk-7的OpenJDK实现Java类在HotSpot的内部实现进行分析。

HotSpot内存划分

在HotSpot实现中,内存被划分成Java堆、方法区、Java栈、本地方法栈和PC寄存器几个部分:
1、Java栈和本地方法栈用于方法之间的调用,进栈出栈的过程;
2、Java堆用于存放对象,在Java中,所有对象的创建都在堆上申请内存,并被GC管理;
3、方法区分成PermGen和CodeCache:PermGen存放Java类的相关信息,如静态变量、成员方法和抽象方法等;CodeCache存放JIT编译之后的本地代码;

HotSpot对象模型

HotSpot JVM并没有根据Java对象直接通过虚拟机映射到新建的C++对象,而是设计了一个oop/klass model,其中oop为Ordinary Object Pointer,用来表示对象的实例信息;klass用来保存描述元数据。

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

从ClassLoaderCase获取一个类加载器classLoader,然后调用loadClass方法,对类进行加载

JVM 源码分析05

1、loadClass方法实现了双亲委派的类加载机制,如果需要自定义类加载器,建议重写内部的findClass方法,而非loadClass方法; (看以上源码,可以发现 ,调用父类loadClass方法对class类进行加载)
2、通过debug,可以发现loadClass方法最终会执行native方法defineClass1进行类的加载,即读取对应class文件的二进制数据到虚拟机中进行解析;

 

class文件的解析

Java中的defineClass1方法是个native方法,说明依赖于底层的实现,在HotSpot中,其实现位于ClassLoader.c文件中,最终调用jvm.cpp中的jvm_define_class_common方法实现,核心的实现逻辑如下:


JVM 源码分析05

把class对应的二进制文件读取到虚拟机中进行解析

1、验证全限定类名的长度,最大为(1 << 16) -1,如果长度超过 65535,就会抛出java/lang/NoClassDefFoundError异常,主要原因是constant pool不支持这么长的字符串;
2、SystemDictionary::resolve_from_stream处理stream数据流,并生成Klass对象。内部通过ClassFileParser.cpp的parseClassFile方法对class文件的数据流进行解析
1、验证当前magic为0xCAFEBABE;
2、获取class文件的minor_version、major_version,并判断当前虚拟机是否支持该版本;
3、通过parse_constant_pool方法解析当前class的常量池;
4、解析当前class的access_flags;
5、解析当前class的父类;
6、解析当前class的接口;
7、....

class数据流解析完成后,通过oopFactory::new_instanceKlass创建一个与之对应的instanceKlass对象,new_instanceKlass实现如下

JVM 源码分析05

1、其中instanceKlassKlass::allocate_instance_klass方法会初始化一个空instanceKlass对象,并由后续逻辑进行数据的填充
2、但是发现该方法的返回类型并非是instanceKlass,而是klassOop类型;
3、allocate_instance_klass方法的实现如下:

JVM 源码分析05

创建出InstanceKlass对象,然后设置对象的默认值,也就是进行数据的填充

 

Klass对象如何创建?

上述的instanceKlass对象由Klass::base_create_klass_oop方法进行创建,实现如下:

JVM 源码分析05

1、allocate_permanent方法默认在PermGen分配内存,instanceKlass对象保存在永久代区域;
2、Klass的as_klassOop方法可以获取对应的klassOop,那klassOop到底是什么?

klassOop相当于Java中的class,一个klassOop对象包含header、klass_field和Klass。

JVM 源码分析05

instanceKlassKlass

instanceKlassKlass在实现上继承了klassKlass类

全局只存在一个instanceKlassKlass对象,虚拟机启动时,会在Universe::genesis方法中初始化。

1、方法Universe::klassKlassObj()获取klassKlass对象;
2、方法base_create_klass负责创建instanceKlassKlass对象,并返回对应的klassOop;
3、方法java_lang_Class::create_mirror分配mirror,类似于一个镜像,在java层面可以访问到

 

klassKlass

klassKlass在实现上继承了Klass类

JVM 源码分析05

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

JVM源码分析之Java对象的创建过程

Java中的new关键字对应jvm中的new指令,定义在InterpreterRuntime类中,实现如下:

new指令的实现过程:
1、其中pool是AAA的constant pool,此时AAA的class已经加载到虚拟机中,new指令后面的#2表示BBB类全限定名的符号引用在constant pool的位置;
2、方法pool->klass_at负责返回BBB对应的klassOop对象,实现如下:

如果常量池中指定位置(#2)的数据已经是个oop类型,说明BBB的class已经被加载并解析过,则直接通过(klassOop)entry.get_oop()返回klassOop;否则表示第一次使用BBB,需要解析BBB的符号引用,并加载BBB的class类,生成对应的instanceKlass对象,并更新constant pool中对应位置的符号引用;
3、klass->check_valid_for_instantiation可以防止抽象类被实例化;
4、klass->initialize实现如下:

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

this_oop->class_initializer()可以获取静态代码块入口,最终通过JavaCalls::call执行代码块逻辑,再下一层就是具体操作系统的实现了

JVM 源码分析05

JVM 源码分析05

instanceOopDesc

当在Java中new一个对象时,本质是在堆内存创建一个instanceOopDesc对象。

JVM 源码分析05

当然,这只是 oopDesc的部分实现,oopDesc包含两个数据成员:_mark 和 _metadata。
1、_mark是markOop类型对象,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致,更具体的实现可以阅读 《java对象头的HotSpot实现分析》
2、_metadata是一个联合体,其中wideKlassOop和narrowOop都是指向InstanceKlass对象的指针,wide版是普通指针,narrow版是压缩类指针(compressed Class pointer)

JVM 源码分析05

4、如果当前类重写了finalize方法,且非空,需要把生成的对象封装成Finalizer对象并添加到 Finalizer链表中,对象被GC时,如果是Finalizer对象,会将对象赋值到pending对象。Reference Handler线程会将pending对象push到queue中,Finalizer线程poll到对象,先删除掉Finalizer链表中对应的对象,然后再执行对象的finalize方法;

 

JVM源码分析之JVM启动流程

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05

 

JVM.dll文件的装载

初始化虚拟机中的函数调用,即通过JVM中的方法调用JVM.dll文件中定义的函数,实现如下:

JVM 源码分析05

2、虚拟机参数解析

装载完JVM环境之后,需要对启动参数进行解析,其实在装载JVM环境的过程中已经解析了部分参数,该过程通过ParseArguments方法实现,并调用AddOption方法将解析完成的参数保存到JavaVMOption中,JavaVMOption结构实现如下:

这里对-Xss参数进行特殊处理,并设置threadStackSize,因为参数格式比较特殊,其它是key/value键值对,它是-Xss512的格式。后续Arguments类会对JavaVMOption数据进行再次处理,并验证参数的合理性。

参数处理

Arguments::parse_each_vm_init_arg方法负责处理经过解析过的JavaVMOption数据,部分实现如下:

JVM 源码分析05

1、如果参数为-XX:+UseSerialGC -XX:+UseParallelGC,由于UseSerialGC和UseParallelGC不能兼容,JVM启动时会抛出错误信息;
2、如果参数为-XX:+UseConcMarkSweepGC -XX:+UseParNewGC,其中UseConcMarkSweepGC和UseParNewGC可以兼容,JVM可以正常启动;

JVM 源码分析05

JVM 源码分析05

2、加载主类的class

Java运行方式有两种:jar方式和class方式。

jar方式

JVM 源码分析05

1、调用GetMainClassName方法找到META-INF/MANIFEST.MF文件指定的Main-Class的主类名;
2、调用LoadClass方法加载主类的class文件;

class方式

1、调用NewPlatformString方法创建类名的String对象;
2、调用LoadClass方法加载主类的class文件;

3、查找main方法

通过GetStaticMethodID方法查找指定方法名的静态方法,实现如下

JVM 源码分析05

JVM 源码分析05

JVM 源码分析05