由static带来的一连串问题

本文深入探讨Java类的初始化过程,包括加载、准备和初始化三个阶段的区别。重点讲解了类初始化的触发条件及类中静态成员的初始化时机。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

起因

写代码的时候碰到这么一种需求

  • 一个方法的调用次数很高
  • 这个方法内需要使用一个map

本能的想到了将这个map在最开始的时候就初始化。方法就是扔在一个类里面,使用一个静态成员来定义这个map,那么问题就来了,我这个map实际上不可能只是使用一个空的map,肯定还是需要在初始化的时候往里面扔进去一些数据的,那就无可避变的涉及到了静态代码块。可以看看如下代码:

Testjingtai.java

public class TestJingtai {

    public static Map<String,String> map = new HashMap<String, String>();

    static {
        System.out.println("init test jingtai");
        map.put("1","1");
        map.put("2","1");
    }
}

Main.java

public class Main {
    public static void main(String[] args){
        System.out.println(TestJingtai.map);
    }
}

运行结果

init test jingtai

其实当时我是怀着疑问的态度的,写了段儿代码就是想尝试一下,看看到底能不能够初始化这个代码块儿。最后结果出来了,发现是能够初始化的。但是我仍然疑惑,为啥能初始化呢?啥时候初始化呢?底子薄,想不明白,拎了本《深入理解java虚拟机-第二版》瞅了瞅,里面正好有讲类的初始化这一块儿,感兴趣的读了读。

在说初始化前,还是得先看看,class是怎样从一个文件被虚拟机放到内存里面的,如果不清楚这个流程,那么就无从讲起初始化,还是会迷糊。

类的生命周期

从图中能够看到整个类的生命周期,其中涉及到这个问题的我打了红色标识。所以重点关注一下加载准备以及初始化。

以前犯迷糊的一个重要原因就是认为,加载之后类就初始化了 ,实际上从这个图来看,这种想法是错误的。加载!=初始化

加载

那加载的过程中干了些啥事儿呢?一句话概括就是:拿到一个类的二进制字节流,并将其转化为方法区中的Class对象。说白了就是从外存到内存的一个过程(外存并不仅限制与.class文件)。

准备

准备阶段可以理解为为变量赋初始值的阶段,这个地方有三个点值得注意:

  • 赋初始值的变量仅包括类的static变量,而不包括实例变量
  • 赋初始值的变量不是用户定义的变量,而是系统自定义的“0”值
  • 如果引入了final static,那么赋初始值的就是用户定义的变量

下表为基本数据类型的0值(摘自深入理解java虚拟机)

数据类型0值
int0
long0L
short(short)0
char‘u\0000’
byte(btye)0
booleanfalse
float0.0f
double0.0d
referencenull

初始化

简要概括这个阶段的话,可以理解为虚拟机为类中的变量赋上用户所定义的值,并且执行类中的所有静态语句块儿,即clinit方法的一个阶段。而触发初始化的条件也有明确的说明,就是以下五条(摘自深入了解java虚拟机)。

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这个clinit方法有些意思,我最开始认为这个方法会连带着这个类中的非静态语句块一起执行,但是实际上并非如此,可看如下代码:

Testjingtai.java

public class TestJingtai {

    public  static String s = "11111";

    {
        System.out.println("init nonestatic code");
    }
}

Main.java

public class Main {
    public static void main(String[] args){
        System.out.println(TestJingtai.s);
    }
}

运行结果

11111

看到没,没有执行非静态代码块儿中的东西。也就是说,类的初始化实际上是不会初始化非static代码块儿的。那这些代码块儿是在啥时候执行的呢?是在创建类的实例对象的时候执行的,实例化的时候会执行一个名为init的方法。在创建类实例的过程中,是先初始化类,然后再执行类中的代码块儿,最后执行类的构造函数。如下图所示:
执行顺序

如果想要更深入了解init的话,可以瞅瞅官方的虚拟机文档
如果想要了解clinitinit区别的话,也可以看看这篇文档

把这些都弄清楚后,实际上一直困扰着自己的类中成员(包括父类),代码块儿,构造函数的初始化顺序基本上也就明了了。

最后

类的初始化中有这么一点:
生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

那常量池里面都有些啥呢? 是不是所有声明为final的变量都能在编译期就扔到常量池里面呢?

可以看看 这里,这是最新的jvm,版本号是8。

也可以瞅瞅这篇文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值