起因
写代码的时候碰到这么一种需求
- 一个方法的调用次数很高
- 这个方法内需要使用一个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值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘u\0000’ |
byte | (btye)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
初始化
简要概括这个阶段的话,可以理解为虚拟机为类中的变量赋上用户所定义的值,并且执行类中的所有静态语句块儿,即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的话,可以瞅瞅官方的虚拟机文档
如果想要了解clinit与init区别的话,也可以看看这篇文档
把这些都弄清楚后,实际上一直困扰着自己的类中成员(包括父类),代码块儿,构造函数的初始化顺序基本上也就明了了。
最后
类的初始化中有这么一点:
生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
那常量池里面都有些啥呢? 是不是所有声明为final的变量都能在编译期就扔到常量池里面呢?
可以看看 这里,这是最新的jvm,版本号是8。
也可以瞅瞅这篇文章。