一个关于JVM类加载过程的案例

通过谜题探索答案,是一个很好的学习过程☝️。首先,我们就从一个案例出发:

public class Test {
	public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        
		System.out.println("[main]num1: " + num1);
        System.out.println("[main]num2: " + num2);
	}
}

class Singleton {
	
	public static int num1;
    public static int num2 = 0;
	
	private static Singleton singleton = new Singleton();
	
	private Singleton() {
		num1++;
		num2++;
		
		System.out.println("[cons]num1: " + num1);
		System.out.println("[cons]num2: " + num2);
	}
	
	public static Singleton getInstance() {
		return singleton;
	}
	
}

请观察上面的代码,你觉得输出会是什么?🤔

也许你会觉得这报错了吧?🤔

其实不会,它的输出如下:

[cons]num1: 1
[cons]num2: 1
[main]num1: 1
[main]num2: 1

为什么不报错呢?num1不是没有赋初值么?

不要着急,我们一起来探索答案
我们知道,Java是一个半编译半解释的语言,Java代码先编译成class文件,然后再由虚拟机解释运行。其中,把class文件中的数据加载到内存中准备执行的过程就叫做JVM的类加载机制。

JVM的类加载机制,有以下几个阶段:

在这里插入图片描述

关于类加载机制的具体过程是一个长篇大论的内容,本文只关注于我们谜题中涉及的部分。

我们要知道,在上图的 准备(Preparation) 阶段,虚拟机会为每一个静态变量赋予默认的初始值(也称零值

注意!,此时的虚拟机并不关心程序员是否为此变量赋初始值或赋了什么值

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short) 0double0.0d
char‘\u0000’referencenull
byte(byte) 0

因此,之前的谜题中,num1即使没有赋值虚拟机也会为它赋上零值。

到了 初始化(Initialization) 阶段时,虚拟机才会去关注程序员所赋予的初始值!而且还要注意一点:所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们,关于这点本文不详谈。

大牛们到这里可能已经觉得不耐烦了,等等别走!,真正的好戏才刚刚开始!

如果把上面代码中的一行换个顺序:

public class Test {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        
        System.out.println("[main]num1: " + num1);
        System.out.println("[main]num2: " + num2);
    }
}

class Singleton {
    
    public static int num1;
    
    //我刚才在这里
    //public static int num2 = 0;
    
    private static Singleton singleton = new Singleton();
    
    private Singleton() {
        num1++;
        num2++;
        
        System.out.println("[cons]num1: " + num1);
        System.out.println("[cons]num2: " + num2);
    }
    
    //现在到这儿来啦!
    public static int num2 = 0;
    
    public static Singleton getInstance() {
        return singleton;
    }
    
}

这样,输出又是什么呢?

直接甩答案:

[cons]num1: 1
[cons]num2: 1
[main]num1: 1
[main]num2: 0    //ATTENTION!!!

嗯~~有点儿意思

这其中的陷阱在于我们一定要记住,类加载时,对变量的准备和初始化都是按照 顺序执行 !的

如上代码中,变量的顺序为:1️⃣num1,2️⃣singleton,3️⃣num2

我们按照类加载机制和变量的顺序捋一遍这段代码的加载过程

首先在准备阶段:

  1. num1赋予零值,num1 == 0

  2. singleton赋予零值,singleton == null

  3. num2赋予零值,num2 == 0

接下来到了初始化阶段,注意顺序还是那个顺序:

  1. num1没有人为赋值,num1 == 0

  2. singleton == new Singleton(),此时执行构造方法Singleton()。在构造方法中num1++; num2++;因此会输出

    [cons]num1: 1
    [cons]num2: 1
    
  3. 最后到了num2初始化,它被人为初始化为0,所以最终num2 == 0,输出

    [main]num1: 1
    [main]num2: 0
    

希望这篇文章能够让你学到知识!当然,JVM的类加载机制远不止这些,还有很多有趣的谜题等着我们去探索!

最后,一个小练习,上面的代码如果这样修改,会输出什么?

public class Test {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        
        System.out.println("[main]num1: " + num1);
        System.out.println("[main]num2: " + num2);
    }
}

class Singleton {
    
    public static int num1 = 1;

    private static Singleton singleton = new Singleton();
    
    private Singleton() {
        num1++;
        num2++;
        
        System.out.println("[cons]num1: " + num1);
        System.out.println("[cons]num2: " + num2);
    }
    
    //现在到这儿来啦!
    public static int num2 = 0;
    
    public static Singleton getInstance() {
        return singleton;
    }
    
}

聪明的你一定能给出答案,正确答案就是:

[cons]num1: 2
[cons]num2: 1
[main]num1: 2
[main]num2: 0

参考资料:

[1] 张龙,《深入理解JVM》,http://www.iprogramming.cn/

[2] 周志明,《深入理解java虚拟机:JVM高级特性与最佳实践》(第2版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值