前言
Java编写中,一定会遇到一些初始化顺序的问题,静态变量,静态初始化块,普通成员变量,普通初始化块以及构造器,本文将会使用例子的方式,来详细展示一下 java 类里面各个部分的初始化顺序以及不触发初始化的一些情况
正文
我先上代码,后面会详细解释
测试代码
没有继承,并且会触发类的初始化
public class NoExtendTest {
public static String staticField = "静态变量";
public String normalField = "普通成员变量";
static {
System.out.println(staticField);
System.out.println("静态初始化块");
}
{
System.out.println(normalField);
System.out.println("普通初始化块");
}
public NoExtendTest(){
System.out.println("构造器");
}
}
class Main {
public static void main(String[] args) {
// 可以看到,当初始化一个实例的时候,一次的加载顺序是
// 静态变量
// 静态初始化块
// 普通成员变量
// 普通初始化块
// 构造器
new NoExtendTest();
}
}
有继承,并且会触发初始化的情况
public class ExtendTest {
public static void main(String[] args) {
// 父类静态变量
// 父类静态初始化块
// 子类静态变量
// 子类静态初始化块
String test = Child.childStaticField;
// 父类静态变量
// 父类静态初始化块
// 子类静态变量
// 子类静态初始化块
// 父类变量
// 父类初始化块
// 父类构造器
// 子类变量
// 子类初始化块
// 子类构造器
new Child();
}
}
class Super {
public static String parentStaticField = "父类静态变量";
public static final String gst = "aaa";
public String parentField = "父类变量";
static {
System.out.println( parentStaticField );
System.out.println( "父类静态初始化块" );
}
{
System.out.println( parentField );
System.out.println( "父类初始化块" );
}
public Super() {
System.out.println( "父类构造器" );
}
}
class Child extends Super {
public static String childStaticField = "子类静态变量";
public String childField = "子类变量";
static
{
System.out.println( childStaticField );
System.out.println( "子类静态初始化块" );
}
{
System.out.println( childField );
System.out.println( "子类初始化块" );
}
public Child()
{
System.out.println( "子类构造器" );
}
}
存放在数组中,不触发初始化的情况
public class NoInit {
public static void main(String[] args) {
// 不会触发初始化
Super[] test = new Super[10];
}
}
class Father {
public static String parentStaticField = "父类静态变量";
static
{
System.out.println(parentStaticField);
System.out.println("父类静态初始化块");
}
public Father() {
System.out.println( "父类构造器" );
}
}
class Son extends Super {
public static String sonStaticField = "子类静态变量";
static
{
System.out.println(sonStaticField);
System.out.println("子类静态初始化快");
}
public Son() {
System.out.println( "子类构造器" );
}
}
访问类中的(static final)常量,不会触发初始化,但是访问类中的 static 的对象是会触发初始化的
public class FinalInit {
public static void main(String[] args){
// Final变量
System.out.println(FinalFieldClass.finalField);
}
}
class FinalFieldClass {
public static String staticField = "静态变量";
static {
System.out.println("静态初始化块");
}
public static final String finalField = "Final变量";
}
上面的代码我上传 github 了
类的生命周期
如上图所示,类的生命周期包含了加载
,连接
(验证,准备,解析),初始化
,使用
,卸载
阶段,连接阶段包含了验证
,准备
和解析
,其中类的加载
,验证
,准备
,初始化
和卸载
这五个阶段的顺序是确定的,但是解析
阶段不一定,可能会在初始化之后再开始解析,这主要是因为Java语言支持的运行时绑定而导致的
我说一下类的加载连接和初始化阶段所干的事情
1.加载阶段
加载阶段,主要完成了以下三个任务:
- 通过一个类的全限制名类获取
.class
的二进制字节流 - 将这个字节流转化为存储在方法去的类结构
- 在堆内存中生成这个类的
Class
(java.lang.Class)对象,作为方法区中类结构的入口
2.连接阶段
- 验证:用于确保加载类的正确性,包括文件格式的验证,源数据验证,字节码验证和符号引用验证,验证阶段是很重要,但是并不是必须的,所以可以通过参数
-Xverifyone
来关闭大部分的验证,可以提高加载的时间 - 准备:为类的静态变量分配内存,并将其初始化为默认值,这些内存都在方法区中分配
- 这个阶段初始化的是类变量,不包含示例变量,实例变量的初始化时和对象初始化一起在堆内存中分配
- 同事,该阶段初始化的值是数据类型的默认值,而并非代码中制定的值
- 如果字段是被
static final
所修饰,即我们口中的常量,那么在该阶段会被初始化为 java 代码中指定的值
- 解析:解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,解析动作主要是针对类,接口,类方法,接口方法,方法类型,方法句柄和调用限定符7类符号引用进行的,所谓符号引用,就是一组符号来描述目标,可以是任何字面量,直接引用就是直接指向目标指针,相对偏移量或一个间接定位到目标的句柄
3.初始化(这里的初始化才真正使我们上面代码所能体现的)
初始化阶段是执行类构造器
初始化时机:
- 遇到
new
,getStatic
(获取一个静态变量,注意不是static final),putStatic
(为一个static变量赋值操作)和invokeStatic
(调用一个静态的方法)这4条字节码指令,如果类没有初始化,那么就会对其进行初始化 - 使用 java.lang.reflect 包中的方法对类进行反射调用的时候,如果类还没有初始化,也会对其进行初始化操作
- 当初始化一个类时,如果其父类没有初始化,则会县初始化其父类,在初始化其本身
- 当虚拟机启动时,需要制定一个主类(main方法所在的类),虚拟机会首选初始化这个主类
- 当使用JDK1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic 、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
注意:
1. 调用类的静态字段,只有直接定义该字段的类才会初始化,如通过子类调用父类的静态变量,只有父类会初始化
2. 静态常量不会触发初始化加载过程,static final 修饰的字段在 Javac 时生成 ConstantValue 属性,在类加载的准备阶段根据 ConstantValue 的值为该字段赋值,它没有默认值,必须显式地赋值,否则 Javac 时会报错。可以理解为在编译期即把结果放入了常量池中
对象的实例化
对象的实例化过程包含了类的加载过程,分配对象的内存,设置默认值,赋值以及执行构造方法
1.加载类
2.在堆中为对象分配存储空间
3.对象中的字段设置默认值
4.对字段进行赋值操作
5.执行构造方法
以上是我对于类加载的一些理解,如果那里有问题欢迎大家提出来共同探讨一下
同时本文中引用了另一篇文章的对类加载的解释过程 https://my.oschina.net/kun123/blog/1031380