类在JVM的生命周期包括加载、链接和初始化。也就是loading、linking和initializing。前一篇博文已经介绍了Class文件和类加载器的内容,本文主要介绍另外的linking和initializing。同时也会记录分析一个关键性问题,就是静态变量和成员变量对于赋默认值和赋初始值的区别,这个区别会导致一些问题,为了避免这些问题,会用到volatile关键字。 说明:最近在补齐一些基础知识,这些内容都是一些旧的知识,所以文章以笔记的形式为主,将会与以往那种精耕细作的方式有所区别,为的是提高效率,快速学习。 关键字:java,jvm,linking,verification,preparation,resolution,initializing,volatile。
静态变量
Linking
1、Verification,文件校验,校验字节码是否符合JVM规范。
2、Preparation,给静态成员变量赋默认值。例如int默认值0,long默认值0,float默认值0.0。
3、resolution,解析。将类、方法、属性等符号引用解析为直接引用。Class文件字节码中介绍过,常量池中包含各种符号引用,解析就是把这里的符号引用转换为指向内存地址和指针的过程。很多都是动态绑定,只有new出来才会知道内存位置。
Initializing
1、调用类初始化代码<init>,对静态变量赋初始值。
成员变量
private int m = 8;
上面都是针对静态变量。成员变量需要类先实例化以后才会执行。
1、实例化时会先给对象申请内存,在这时候给成员变量赋默认值0。
2、当申请完内存以后,会执行构造方法,才会赋值初始值8。
总结
1、load - 默认值 - 初始值
2、new - 申请内存 - 默认值 - 初始值
单例模式
DCL 单例,Double Check Loading 双重检查。
1、第一次检查,如果instance等于null,说明没有任何线程对它进行初始化。
2、第二次检查,就是在我上锁期间,可能有其他线程对instance进行了初始化,如果仍然为null,说明也没有任何线程对它进行初始化。我就可以在上锁内部放心的进行初始化了。
instance声明时是否需要加volatile?
如果没有加volatile,初始化逻辑在运行一半的时候,instance不为空了,它被赋值默认值了,但是还没有赋初始值。
这里仍旧是在使用上面的知识,静态变量在类linking和initializing的过程值的变化,成员变量也会在对象创建期间有一个默认值和初始值的状态的变化。
这时候,恰好,另外线程进来发现instance不为空,就不再初始化,直接使用instance了,而这时候这个线程拿到的其实是instance的默认值,不是初始值。就会造成严重的问题,相当于我的类的对象的所有成员全都还是默认值,例如金额long类型的,此时还是0呢,这时候另一个线程直接当它是有效的值去用,就产生问题了。
所以,我要保证,其他线程来的时候,要拿到instance的赋完初始值以后准备成熟的对象,再去使用就没问题了。那么如何保证呢?就是这个过程临时状态不被外界所侵扰,那么就需要volatile关键字来帮忙了。
volatile
volatile是如何保证对象创建时的临时状态不被外界所侵扰呢?它的内部原理实际上是指令重排。
我们编写了一个简单的类,main方法中只有对当前类的实例化,并用实例接收。使用IDEA插件查看字节码,查看到main函数的执行字节码:
1、new 申请了内存。
2、invokespecial,是调用构造方法init。
3、astore_1,把内存赋值给对象t。
指令重排就是第2和第3步顺序发生了交替,就是先把内存赋给了对象t,然后再构造方法init。
volatile关键字就是为了强制避免指令重排的情况,具体实现细节要研究Java内存模型。