Java类加载之Class对象到Klass模型

这是没啥用的知识第一篇技术文章,小编准备聊一聊Java中的类加载机制。在写Java程序的时候,你不用知道类加载机制是啥,它从哪里来,要到哪里去,虽然这知识没啥用 ,但是,面试(吹水)或许能顶一顶

每一个Java程序员在刚开始学Java的时候,第一个了解到的,一定是Java的灵魂——JVM。你写的JavaBug,I‘M sorry,Java代码最终一定是经过Java编译器编译成class文件后,交付给JVM进行逻辑执行。

那么你一定一定要疑惑(否则写不下去了),Java类是如何与JVM进行交互的呢?下面我们一起来学习一下这些学完就忘的知识。

注:文章中所述的技术,JDK版本皆为JDK1.8

小贴纸
JVM只认识Class文件,也就是说,任何一门计算机语言,只要它最后将代码编译成class文件,都能被JVM所执行。
Kotlin、Groovy、JRuby、Jython、Scala等语言就是如此。

一、什么是Class文件
1. class文件是Java代码经过Javac编译后生成的字节码文件,如下图所示。

图1

2. class文件主要包含了魔数、JVM版本号、常量池、常量池计数器、访问标识等信息,如下图所示(截取自 Java虚拟机规范)

在这里插入图片描述

  • magic:魔数,占4字节。作用是判断这个文件是否为一个虚拟机所能接受的class文件
  • minor_version::副版本号,占2字节,最小支持版本号。
  • major_version:主版本号,占2字节,最大支持版本号。
  • constant_pool_count:常量池计数器,记录常量池表中的成员数,它的值为常量池表中的成员数+1,常量池表的索引值只有在大于0并且小于constant_pool_count时才认为是有效。
  • constant_pool:常量池,包含class文件结构及其子结构中所引用的所有字符串常量、类或接口、字段名和其他常量。
  • access_flag:访问标志,用于表示类或接口的访问权限及属性。
  • this_class:类索引。
  • super_class:父类索引。
  • interfaces_count:接口计数器。
  • interfaces_count[]:接口表。
  • fields_count:字段计数器。
  • fields:字段表。
  • methods_count:方法计数器。
  • methods[]:方法表。
  • attributes_count:属性计数器。
  • attributes[]:属性表。
3. 每一个Java类在JVM中都会对应创建一个C++类实例,我们称这个C++类为Klass实例(对应hotspot源码中的instanceKlass类),Klass实例里面存储了java类中所描述的方法、字段、属性等。如下图所示,instanceKlass的字段皆为存储java类文件中的数据所设计,详见hotspot源码中 instanceKlass.hpp文件。

在这里插入图片描述

小贴纸
JVM在创建InstanceKlass对象时,为其申请的内存空间,远超instanceKlass本身所需要得空间,这是因为InstanceKlass还要存虚表、接口表、以及Java类中的引用类型表

二、class类加载的过程

你每写的一个Java类,在JVM中,都有一个对应的C++类实例,这个C++类实例,我们管它叫Klass类,它存储了类的元信息(常量池、属性、方法等)。

1. 加载阶段
  1. 通过类的全限定名获取java类编译后生成的class文件,加载进JVM,并解析class文件。
  2. 解析完成后,JVM便会在内部创建一个与Java类对等的类模板对象 instanceKlass实例(也是C++的一个类,里面保存了java类的常量池、方法、属性等信息)。

小贴纸
解析常量池、解析Java类字段、解析Java类方法这些JVM的精华部分感兴趣的同学,也可以关注微信公众号:云下凤澜。相关文章会在公众号上更新。
下面奉上hotspot源码解析常量池、字段、方法并创建对应的Klass对象部分,代码皆在ClassFileParser.cpp的 parseClassFile方法中,有兴趣的同学可以自己看一看。以下仅截取部分主要代码

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    Handle protection_domain,
                                                    KlassHandle host_klass,
                                                    GrowableArray<Handle>* cp_patches,
                                                    TempNewSymbol& parsed_name,
                                                    bool verify,
                                                    TRAPS) {

  // Constant pool   解析常量池
  constantPoolHandle cp = parse_constant_pool(CHECK_(nullHandle));
 //......
    //解析Java类字段
    Array<u2>* fields = parse_fields(class_name,
                                     access_flags.is_interface(),
                                     &fac, &java_fields_count,
                                     CHECK_(nullHandle));
    //......
    //解析Java类方法
    Array<Method*>* methods = parse_methods(access_flags.is_interface(),
                                            &promoted_flags,
                                            &has_final_method,
                                            &declares_default_methods,
                                            CHECK_(nullHandle));

    //....
    // 开始创建与Java对等的Klass对象
    _klass = InstanceKlass::allocate_instance_klass(loader_data,
                                                    vtable_size,
                                                    itable_size,
                                                    info.static_field_size,
                                                    total_oop_map_size2,
                                                    rt,
                                                    access_flags,
                                                    name,
                                                    super_klass(),
                                                    !host_klass.is_null(),
                                                    CHECK_(nullHandle));

}
  1. 通过instanceKlass生成一个镜像类,放在堆区,即instanceMirrorKlass实例(对应hotspot源码中的 instanceMirrorKlass类)。instanceKlass供jvm内部使用,小编认为多生成一个instanceMirrorKlass是因为考虑到运行安全因素,不能直接把类暴露给外部使用,所以弄出了个镜像类实例提供给外部程序调用。

小贴纸:

  1. java类中的静态变量会存储在instanceMirrorKlass类中,instanceMirrorKlass类里面比instanceKlass类多定义了一个静态字段偏移量的属性,可以通过该属性获取静态变量。
    在这里插入图片描述
    2.Java中的数组类在运行时数据区的生成的实例为 方法区:ArrayKlass。堆区:基本类型数组 TypeArrayKlass,引用类型数组ObjArrayKlass 分别对应hotspot源码里的TypeArrayKlass.cpp 与 ObjArrayKlass.cpp类
    3 类加载器是什么时候加载的,如下图,hotspot源码java.c中有一个javaMain方法,javaMain 里面调用了LoadMainClass 方法,你的一切疑惑都在LoadMainClass里面,它的执行逻辑是通过启动类加载器加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain,加载main函数所在的类,启动扩展类加载器、应用类加载器也是在这个时候完成的。
    在这里插入图片描述
2. 验证

验证主要就是对Java虚拟机定义的一些约束进行校验,如果校验不通过就抛出异常。

  1. 静态约束:

  2. 结构化约束

    两种约束的校验都能单独写一篇文章了,这里就不做赘述了,有想深入了解的可以给作者留言或者关注公众号:云下凤澜。

3. 准备
  1. 创建类或接口的静态字段,并用默认值初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令。
数据类型默认值
byte(byte)0
shot(shot)0
long0L
char‘\u0000’
int0
float0.0f
double0.0d
boolean0(false)

final修饰类的变量,已经不会再 发生变化,所以在准备阶段就进行赋值了,就没有赋初值这个操作了。

4. 解析
  1. 我们从各种某度的文章里总会看到,解析的主要过程就是将符号引用替换为直接引用。那么什么是符号引用呢? 如下图,下图采用了jclasslib插件展示一个Java类文件编译后产生的class文件信息。我们可以看到方法里面字节码指令后面的 #1等符号就是我们常说的符号引用在这里插入图片描述
    这个符号引用其实就是常量池中的索引(例如#1指向的就是常量池中的第一个类或方法) 如下图所示在这里插入图片描述

,JVM会在准备阶段将这些索引符号替换为直接内存地址。以供后续JVM指令进行调用。

  1. 都有那些虚拟机指令需要进行符号引用的解析呢?

anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic,执行上述任何一条指令都需要对它的符号引用进行解析。

如果在某个符号引用解析过程中发生错误,那么应该在使用该符号引用的程序处抛出IncompatibleClassChangeError或者其子类的异常

5. 初始化
  1. 初始化就是 执行类的静态代码块,并且完成静态变量的赋值,我们可以看到,如下图,如果我们的代码里要是有静态变量,并且对静态变量进行赋值了,那么生成的字节码文件中就会有clinit方法。这个clinit就是执行静态变量赋值的指令,而且方法中语句的先后顺序与代码的编写顺序相关在这里插入图片描述

既然初始化的时候可以直接对变量进行赋值,那我们是否可以跳过准备阶段,直接在初始化阶段进行赋值。因为准备阶段主要是赋初值,那我们可以直接要我们写的值,不要初始值。
答案当然是不行,原因如下

初始化阶段主要是依靠clinit方法生成的指令进行赋值,但是如果我们定义一个空的静态变量,那clinit方法中就不会生成这个静态变量相关的赋值代码。如下图,所以这时就需要准备阶段给这个静态变量初始化、赋初值,否则这个变量就丢掉了。在这里插入图片描述

初始化之后就由JVM的执行引擎进行取指执行了,执行引擎有些过于复杂,以后有机会再分析吧。

总结:

  1. JVM能执行的就是Class文件,所有计算机语言只要最后生成了Class文件,都可以交给JVM执行。Kotlin、Groovy、JRuby、Jython、Scala等语言就是如此。
  2. 由于JVM是由C/C++编写的,所以每一个Java类加载到JVM时都会生成一个对应的C++类,即instanceKlass,存放在方法区(元空间)。同时生成一个instanceKlass的实例对象,即instanceMirrorKlass,放在堆区。
  3. JVM类加载机制分为,加载、验证、准备、解析、初始化五个阶段。
  4. 加载阶段:
    通过类的全限定名获取存储该类的class文件,并对其进行解析
    解析后生成对应的C++模板类,即instanceKlass实例,存放在元空间,用于JVM内部使用
    在堆区生成该类的Class对象实例,即instanceMirrorKlass,用于其他系统或程序进行调用。
  5. 验证阶段主要有静态约束校验,结构化约束校验两种。
  6. 准备阶段主要是对静态变量赋初值的操作
  7. 解析阶段就是将符号引用(常量池索引)替换为直接引用(方法内存地址)
  8. 初始化就是 执行类的静态代码块,并且完成静态变量的赋值。赋值的指令会生成在clinit方法中,当在Java代码中对静态变量赋值了,clinit中才会生成对应的指令。

扩展问题:

  • 类的初始化阶段会不会有线程安全问题?
  • 类加载阶段会使用synchronized锁机制吗?
  • 类加载时延迟偏向锁的原因?
  • 如果实现一个类加载器,不按照类加载器机制实现可不可以?

问君能有几多愁,恰似满屏Bug+需求。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云下牧羊人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值