这里看到的这个题
看起来很简单,就自己写了一下,然后看原文链接还是没看懂,这里自己写一下自己的想法。
我们看一下这个题:
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1 + " count2=" + singleTon.count2);
}
}
乍一看,按照我们的思路应该是输出
count1=1 count2=1
我们输出一下就会发现其实是输出的是:
count1=1 count2=0
其实我们的思路是对的,我们知道为变量赋值是在初始化阶段,这个时候其实我们的这些SingleTon类包括count1,count2的内存其实都已经在准备阶段创建出来了,只是说是默认值,真正的赋值是在初始化阶段。
只不过我们忽略了一个静态变量初始化的顺序问题,JVM加载的时候静态变量是按照顺序加载的,这个类里面一共有三个静态变量,所以这三步是顺序执行的:
1、 private static SingleTon singleTon = new SingleTon();
2、 public static int count1;
3、 public static int count2 = 0;
第一步,加载SingleTon,这里调用了new SingleTon()赋值,也就是会调用构造方法里面的值。
private SingleTon() {
count1++;
count2++;
}
这里的值如果打印的话会发现count1,count2由0变成了1。
这个时候1、SingleTon构造结束。会继续往后执行2跟3。给count1,count2赋值。
第二步,我们看到count1是没有做赋值操作的,那么就不需要进行赋值,保持原来的就可以了,这个时候count1的值是1。
第三步,给count2赋值为0
所以,最终结果是count1=1 count2=0
我们可以验证一下给这三步换个顺序就很明显了~
这个其实是考验我们对虚拟机类加载过程的了解,先画个图。
![](https://img.haomeiwen.com/i6857764/fc82d34d28a4d2a7.png)
一、加载
虚拟机规范要求,虚拟在这个阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。也就是ClassLoader起作用的阶段。
二、连接
验证、准备、解析称为连接阶段。
1、验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2、准备
准备阶段是正是为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存将都在方法区中进行分配。这里的进行内存分配的类变量是被static修饰的变量,而不包括实例变量,实例变量将会在对象实例化时分配在堆中。而final static修饰的会直接赋值为对应的值。
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
3、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,比如在方法A中使用方法B,A(){B();},这里的B()就是符号引用,初学java时我们都是知道这是java的引用,以为B指向B方法的内存地址,但是这是不完整的,这里的B只是一个符号引用,它对于方法的调用没有太多的实际意义,可以这么认为,他就是给程序员看的一个标志,让程序员知道,这个方法可以这么调用,但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用。
三、初始化
初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行勒种定义的Java程序代码。
初始化结束后一个类的加载就结束了。这个时候我们的Class类和类变量(static变量)已经在方法区了。
这个时候我们再回过头来看上面的那个题其实就很清晰了,其实就是一个初始化阶段赋值顺序的问题。
class SingleTon {
private static SingleTon singleTon = new SingleTon(); // 2️⃣ 给singleTon分配空间并设置初始值为null 5️⃣singleTon初始化为 new SingleTon()
public static int count1; // 3️⃣ count1分配空间并设置初始值为0 7️⃣ count1没有初始化值,故保持当前的值,当前值为1
public static int count2 = 0; // 4️⃣ count2分配空间并设置初始值为0 8️⃣ count2 初始化赋值为0
private SingleTon() { // 6️⃣ SingleTon构造方法
//当前 count1 = 0 count2 = 0
count1++;
count2++;
//++完之后 count1 = 1 count2 = 1
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance(); // 1️⃣ 加载SingleTon类; 9️⃣ 调用getInstance()方法
System.out.println("count1=" + singleTon.count1 + "count2=" + singleTon.count2);
}
}
我们来分析下这里主要涉及的类加载的阶段:
一、加载
① 加载SingleTon类
二、连接
1、验证
2、准备
② 给singleTon分配空间并设置初始值为null
③ count1分配空间并设置初始值为0
④ count2分配空间并设置初始值为0
3、解析
三、初始化
⑤singleTon初始化为 new SingleTon()
⑥ SingleTon构造方法 (结束后 count1 = 1 count2 = 1)
⑦ count1没有初始化值,故保持当前的值,当前值为1
⑧ count2 初始化赋值为0
⑨ 调用getInstance()方法
网友评论