本文知识点
-
类的状态变化
-
<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地址. 即使现在还没有走过去把东西放下, 别人问的时候,已经可以用那个地址去回答别人了.
源码入口如下图所示