深入理解类的初始化时机

《Java虚拟机规范》

关于在什么情况下需要开始类加载过程的的第一个阶段“加载”,《Java虚拟机规范中》并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

初始化时机一

遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景如下

场景描述
使用new关键字实例化对象的时候
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)。可以换一个说法,读取和设置被final 修饰的基本类型和String类型的静态属性除外。
调用一个类型的静态方法的时候

1.使用new关键字实例化对象的时候

public class InitClass {
    static {
        System.out.println("I was initialized~");
    }
}
public class Main {
    public static void main(String[] args) {
        InitClass initClass = new InitClass();
    }
}
//控制台输出 
I was initialized~

2.读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段)

public class InitClass {
	//注意这里没有使用final修饰
    public static int a = 666;
    static {
        System.out.println("I was initialized~");
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println(InitClass.a); //这里是读取
    }
}
//控制台输出
I was initialized~
666

稍安勿躁,我们去常量池一探究竟,不明常量池可以先看这篇文章,作者真的是天秀,https://blog.csdn.net/wangtaomtk/article/details/52267548

javac编译下这个class文件,然后再javap -v 反编译 InitClass这个文件
惊了!666并不在常量池中

在这里插入图片描述
那我们再看看下面的方法中的汇编(虚拟机能识别的汇编)代码,666居然在类初始化方法中(静态块),甚至可以看到我们静态块里面执行System.out.println(“I was initialized~”),到底怎么回事?

在这里插入图片描述
接下来我将int的数值设置得大一点

public class InitClass {
    //注意这里没有使用final修饰
    public static int a = 66666666;
    static {
        System.out.println("I was initialized~");
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println(InitClass.a);
    }
}
//控制台输出
I was initialized~
66666666

但是这次不一样,这次66666666存放的位置是常量池

在这里插入图片描述
注意这个ldc,ldc表示将int、float或String常量值从常量池中推送至栈顶,说明即使数据存放在常量池也有可能初始化类。

在这里插入图片描述

我查阅了《java虚拟机规范》一书,其中Class文件的属性里有有一个叫ConstantValue属性。

ConstantValue属性作用是通知虚拟机为静态变量赋值。对于非static类型的变量(实例变量)的赋值是在实例构造器方法中进行的。对于类变量有两种方式可以选择(Oracle公司实现javac编译器的选择是如下)

(1)如果同时使用final和staic来修饰一个变量(按照习惯,这里成为常量更贴切)并且这个变量的类型是基本类型或者java.lang.String的话,就会生成ConstantValue属性来进行初始化。

(2)如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在()方法中进行初始化。

原来如此,由于我们没有使用final修饰,则会选择在()方法中进行初始化。自然没有在编译阶段把结果放入常量池,所以会在类初始化阶段给a进行赋值,自然得先初始化

接下来把a这个属性设置为final

public class InitClass {
    public static final int a = 666;
    static {
        System.out.println("I was initialized~");
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(InitClass.a);
    }
}
//控制台输出 
666

3.调用一个类型的静态方法的时候

public class InitClass {
    public static void say(){
        System.out.println("I'm static method~");
    }
    static {
        System.out.println("I was initialized~");
    }
}

public class Main {
    public static void main(String[] args) {
        InitClass.say();
    }
}
//控制台输出 
I was initialized~
I'm static method~

初始化时机二

使用java.lang.reflect包的方法对类型进行反射调用的时候

public class InitClass {
    static {
        System.out.println("I was initialized~");
    }
}
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("org.fenixsoft.classloading.InitClass");
        Method[]  methods = clazz.getMethods();
        for(Method method : methods){
            System.out.println(method.getName());
        }
    }
}

//控制台输出
I was initialized~
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll

初始化时机三

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

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
}
public class SubClass extends SuperClass{
  static {
      System.out.println("Subclass init!");
  }
}
public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}
//控制台输出
SuperClass init!
Subclass init!

初始化时机四

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

public class Main {
    public static void main(String[] args) {
        System.out.println("main method");
    }
    static {
        System.out.println("Main init!");
    }
}
//控制台输出
Main init!
main method

初始化时机五

当使用JDK7新加入的动态语言支持时,如果一java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF__invokeStatic、REF__newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

初始化时机六

当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的默认方法),如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

public class ShowInterfaceInit {
    static {
        System.out.println("interface init!");
    }
}
public interface DefaultInterface {
    ShowInterfaceInit init = new ShowInterfaceInit();
    default void defaultMethod(){
        System.out.println("default Method");
    }
}
public class DefaultInterfaceImpl implements DefaultInterface {
    static {
        System.out.println("DefaultInterfaceImpl");
    }
}
public class Main {
    public static void main(String[] args) {
        new DefaultInterfaceImpl().defaultMethod();
    }
}

//控制台输出
interface init!
DefaultInterfaceImpl
default Method

还有更多有趣的东西,后面接着更!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值