首先分析一个饿汉式的单例模式
public class Singleton {
private static Singleton singleton = new Singleton();
public static int a;
public static int b = 0;
public Singleton () {
a++;
b++;
}
public static Singleton getSingleton() {
return singleton;
}
}
public class Test {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
System.out.println("a = " + singleton.a);
System.out.println("b = " + singleton.b);
}
}
a = 1
b = 0
public class Test {
public static void main(String[] args) {
Singleton singleton = new Singleton();
System.out.println("a = " + singleton.a);
System.out.println("b = " + singleton.b);
}
}
a = 2
b = 1
类的加载过程
加载
“加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证
验证字节码文件的正确性。
准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
public static int value=123; //在准备阶段value初始值为0, 在初始化阶段才会变为123
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
类初始化的时机
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类( 包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。
被动引用的情况
子类引用父类的静态字段,只会触发子类的加载、父类的初始化,不会导致子类初始化
通过数组定义来引用类,不会触发此类的初始化
访问类的常量,不会初始化类
题目分析
- Singleton singleton = Singleton.getSingleton(); // 调用静态方法,触发类的初始化
- 准备阶段 ==== Singleton singleton = null, a = 0, b = 0;
- 初始化阶段 ==== 为类的静态变量赋值,并执行静态代码块 Singleton singleton = new Singleton();
- new Singleton() 执行类的构造方法 ==== a = 1, b = 1;
- a 没有赋值动作, b有赋值动作 ==== a = 1, b = 0;