美文网首页jvmhavareadimportantAndroid资料
JVM源码分析之Java类的加载过程

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

作者: 美团Java | 来源:发表于2016-12-15 20:09 被阅读10285次

简书 占小狼
转载请注明原创出处,谢谢!

趁着年轻,多学习

背景

最近对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编译之后的本地代码;

更详细的相关内容可以阅读《JVM内存的那些事》

HotSpot对象模型

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

Klass

关于为何要设计oop/klass这种二分模型的实现,一个原因是不想让每个对象都包含vtbl(虚方法表),其中oop中不含有任何虚函数,虚函数表保存于klass中,可以进行method dispatch。

oop

oopDesc对象包含两部分数据:_mark 和 _metadata;
1、_mark是markOop类型对象,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致,更具体的实现可以阅读 java对象头的HotSpot实现分析
2、_metadata是一个结构体,wideKlassOop和narrowOop都指向InstanceKlass对象,其中narrowOop指向的是经过压缩的对象;
3、_klass字段建立了oop对象与klass对象之间的联系;

HotSpot如何加载并解析class文件

class文件在虚拟机的整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段,通过ClassLoader.loadClass方法可以手动加载一个Java类到虚拟机中,并返回Class类型的引用。

这里并没有自定义类加载器,而是利用ClassLoaderCase的类加载器进行加载类AAA。

loadClass方法实现

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

class文件的解析

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

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实现如下:

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

1、base_create_klass方法最终通过Klass::base_create_klass_oop方法创建Klass对象,这里是instanceKlass对象,并返回对应的klassOop;
2、k()->klass_part()获取对应的Klass对象,并强制转换成instanceKlass类型的对象;
3、设置instanceKlass对象的默认值;

Klass对象如何创建?

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

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

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

instanceKlass

可以发现,每个instanceKlass对象都有一个ClassState状态,用来标识当前class的加载进度,另外instanceKlass对象中包含了如下字段,描述class文件的信息。

instanceKlassKlass

instanceKlassKlass在实现上继承了klassKlass类

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

虚拟机中所有instanceKlass对象的_klass字段都指向该instanceKlassKlass对象,其初始化过程如下:


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

klassKlass

klassKlass在实现上继承了Klass类

和instanceKlassKlass一样,klassKlass对象也是全局唯一的,虚拟机启动时,会在Universe::genesis方法中初始化,其初始化过程如下:

1、通过base_create_klass创建klassKlass对象,并返回对应的klassOop;
2、set_klass方法把自身设置成_klass;


我是占小狼
坐标魔都,白天上班族,晚上是知识的分享者
如果读完觉得有收获的话,欢迎点赞加关注


相关文章

网友评论

  • N3verL4nd:_metadata是一个联合体。
    在联合体中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。
  • 阿里加多:赞,请问bootstrap是在哪里创建的
  • 6cc01719e0fe:不错不错,收藏了。

    推荐下,源码圈 300 胖友的书单整理:http://t.cn/R0Uflld


  • tinyfox266:class oopDesc {
    private:
    volatile markOop _mark;
    union _metadata {
    Klass* _klass;
    narrowKlass _compressed_klass;
    } _metadata;


    // Fast access to barrier set. Must be initialized.
    static BarrierSet* _bs;
    }

    oopDesc的数据只包含一个markOop,一个Klass和一个指针。但是oopDesc又用于存储一个实例化后的object,那这个object的成员变量的值是存在哪里的?为什么在类oopDesc中没有相应的变量?

    非常感谢!
    美团Java:@tinyfox266 嗯,具体的实例属性数据在二进制中排排在对象头后面:+1:
    tinyfox266:看了下解释器对new指令的执行,明白了是怎么回事。

    因为实例化的object是被分配在堆上的,而且由jvm来管理其生命周期,所以不能直接new一个类型为oopDesc的变量,否则,在出了作用域后,这个变量会被回收。hotspot的做法是,先为实例化的object在堆上分配足够的内存,然后将其基址强制转换为oopDesc的指针。所以,在oopDesc中没有声明存储该object属性的变量。
    美团Java:@tinyfox266 对象的实例数据在对象头后面
  • 疯狂的小蘑菇:oop只有两个部分,那么对象实例的属性值存储在哪里啊?
    疯狂的小蘑菇:@占小狼 oopDesc是作为head存在klassOop中么?
    美团Java:@疯狂的小蘑菇 存在这两部门后面
  • 29679c0b5687:关于类加载的校验我有个疑问,假设存在 A,B 两个类,A 中用了 B,那么在编译的时候必须有 B才行。但是在允许的时候没有 B 也能调用 A 中和 B 无关联的其他的代码,这是什么原因呢?类中的引用只有运行时才会去校验是否存在吗?
    美团Java:@abel533 如果在运行时,A类没有执行上述指令的话,B类是不会被加载到内存的
    美团Java:@abel533 只有在特定情况下会触发类加载,虚拟机中严格规定了有且只有5种情况必须对类进行初始化。

    1、执行new、getstatic、putstatic和invokestatic指令;
    2、使用reflect对类进行反射调用;
    3、初始化一个类的时候,父类还没有初始化,会事先初始化父类;
    4、启动虚拟机时,需要初始化包含main方法的类;
    5、在JDK1.7中,如果java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化;
    29679c0b5687:错字:允许=运行
  • a7520d51614c:真心叼,能有研究得这么深入,佩服佩服,是我学习进步的榜样。。
  • 5f3572e6068d:openjdk hotspot 代码是用subline看的吗?
    美团Java:@仙橙子 效率确实不能比
    5f3572e6068d:@占小狼 我最近也在看,最近才发现右键可以找到对应的类,和ide比起来效率还是偏低。
    美团Java:@仙橙子 眼神厉害
  • 开发者头条_程序员必装的App:感谢自荐!已推荐到《开发者头条》:https://toutiao.io/posts/dkocaz 欢迎点赞支持!
    欢迎订阅《java进阶之路》:https://toutiao.io/subjects/85249

本文标题:JVM源码分析之Java类的加载过程

本文链接:https://www.haomeiwen.com/subject/xcpvmttx.html