本文知识点

  • 类的状态变化

  • <clinit> 方法

  • 实例对象的创建

类的状态变化

类的初始化主要经历加载->链接(验证,准备,解析)->初始化这些阶段,与JVM中相对应的状态如下图所示

instanceKlass.hpp

allocated: 已分配,但尚未链接

loaded: 已加载,并插入到JVM内部类层次体系中,但尚未链接

linked: 已链接,但尚未初始化

being_initialized: 初始化中

fully_initialized: 完成初始化

initialization_error: 初始化过程中出错

加载

.class文件是个二进制文件,我们可以点开.class文件,可以看到各种二进制信息, 右边转成的字符不是很全, 有很多标识位,直接用数字表示的. 右边能看到的, 基本上都是常量池字符串里面的信息

加载.class的源码在classFileParser.cpp 中,如下图所示:

在上图中, 我们可以看到, 有CAFEBABE的定义,版本号的定义, 在往下, 我们可以看到对class文件中的常量池,附录表等解析方法,在此就不在赘述

链接

如我们在out/build 或者别的输出目录中所看到的, class文件都是单独的, class文件中有本类用到的各种静态常量池. 在jvm中还有一个运行时常量池,是各个class都可以访问的. 因为链接最主要的就是把class文件中的静态常量池和运行时常量池关联起来, 把静态符号引用,转成直接内存引用, 然后我们就可以通过地址调用相应的方法,完成操作

链接有三大步,验证, 准备,解析.

验证: 类或接口的二进制信息是否正确, 方法的访问控制, 变量是否初始化等. 通常来说, 只要我们写代码时ide不报错, 基本上就没什么问题, 但有些会自己构造.class文件,交由jvm运行, 所以要验证各种正确性

准备: 在类的准备阶段,将为类静态变量分配内存空间,和赋初始值,但是要注意, 这时候还没有执行任何赋值的代码或者静态代码块!

解析: 如上所述, 把class文件中的静态变量池和jvm内部的运行池给关联起来, 把符号引用换成直接引用

源码位置如下图所示:

 

clinit 方法

clinit方法是初始化的关键所在

这个方法, 我们在java源代码中没有看到过,该方法只能由javac 编译器自动生成和命名,然后自动插入到Class文件中.

clinit方法由编译器收集类变量(静态非final),static 代码块

clinit方法没有任何虚拟机字节码指令可以调用, 它只能在类型初始化阶段被虚拟机隐式调用,全程只调用一次

如果有继承的话,会先初始化父类

其源码如下:

如上图所示,有多个步骤,每个步骤的注释也十分清晰, 强烈建议小伙伴们把源码拉下来阅读一下

其实父类优先于子类初始化,可以步骤7和步骤8中看到,如下图所示:

 

实例对象的创建

实例对象的创建, 这一块相对来说就简单了, 虚拟机遇到new的时候, 从栈顶取得目标对象在常量池中的索引,接着定位到目标类型的类型,接下来,虚拟机看是否已加载采用tlabs/慢速分配(Eden)找一块空地, 然后完成实例数据和对象头的初始化.

流程就是上面个流程,其实也没啥复杂的, 就像我们买东西, 在京东上看了图片(klass) ,然后就买了一个回来(有自己的实例), 如果快递配送的很快,还没来得及想好放哪(还没加载这个类),那就先丢到仓库(Eden区),已经想好怎么放的话(已加载了这个类),那就顺手就给安排了(使用TLABS来分配).

其中要注意的一点就是.一但选好放哪里之后, 就开始在自己的小本本上更新,XXX东西被我放在了XXX地址. 即使现在还没有走过去把东西放下, 别人问的时候,已经可以用那个地址去回答别人了.

源码入口如下图所示