天天看点

几套笔试题下来,终于搞清楚了Java静态代码块-普通代码块-构造方法的执行顺序

目录

验证

为什么静态块先执行,在什么时候执行

JVM类加载过程

静态代码块执行

为什么普通代码块优于构造方法执行

误区探究:静态代码块,会在类被加载时自动执行吗

【结论】

静态代码块 ==> main方法 ==> 普通代码块 ==> 构造方法

  • 如果有继承关系,父类先由于子类执行
  • 静态代码块只在类加载时执行一次(后面再实例化对象时,该类已经加载过,所以静态块不再执行)
几套笔试题下来,终于搞清楚了Java静态代码块-普通代码块-构造方法的执行顺序

验证

class MyClass {
    public MyClass() {
        System.out.println("父类构造方法");
    }

    static {
        System.out.println("父类静态代码块");
    }

    {
        System.out.println("父类非静态代码块");
    }

    public static void method() {
        System.out.println("父类普通方法执行");
    }
}

public class One extends MyClass{
    public One() {
        System.out.println("子类构造方法");
    }

    static {
        System.out.println("子类静态代码块");
    }

    {
        System.out.println("子类非静态代码块");
    }
    public static void method() {
        System.out.println("子类普通方法执行");
    }


    public static void main(String[] args) {
        System.out.println("子类main方法执行");

        MyClass once = new One();
        once.method();

        System.out.println("------------");
        
         //第二次类加载, 父类和子类得静态代码块不再执行
        MyClass twice = new One();
        twice.method();
    }
}
           

执行结果:

  1. 父类静态代码块
  2. 子类静态代码块
  3. 子类main方法执行
  4. 父类非静态代码块
  5. 父类构造方法
  6. 子类非静态代码块
  7. 子类构造方法
  8. 父类普通方法执行
  9. ------------------------ 第二次实例化对象,父类和子类的静态代码块不再执行
  10. 父类非静态代码块
  11. 父类构造方法
  12. 子类非静态代码块
  13. 子类构造方法
  14. 父类普通方法执行
几套笔试题下来,终于搞清楚了Java静态代码块-普通代码块-构造方法的执行顺序

为什么静态块先执行,在什么时候执行

JVM类加载过程

首先,得从JVM类加载器说起。可以参考我之前写过的文章

🔗【探究JVM一】JVM的入口——类加载器子系统

一个类从 .java 到 .class 加载到JVM解释执行,需要经历三大步骤:

几套笔试题下来,终于搞清楚了Java静态代码块-普通代码块-构造方法的执行顺序

加载 ==> 链接 ==> 初始化

其中装载阶段又三个基本动作组成:

  1.     通过类型的完全限定名,产生一个代表该类型的二进制数据流
  2.     解析这个二进制数据流为方法区内的内部数据结
  3.     构创建一个表示该类型的java.lang.Class类的实例

另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。

链接阶段又分为三部分:

  1. 验证,确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),另外还需要进行符号引用的验证。
  2. 准备,Java虚拟机为类变量分配内存,设置默认初始值。
  3. 解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。

当一个类被主动使用时,JVM就会对其初始化,如下六种情况为主动使用:

  1. 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
  2. 当调用某个类的静态方法时
  3. 当使用某个类或接口的静态字段时
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
  5. 当初始化某个子类时
  6. 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
  7. JDK1.7开始提供的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始

    被动使用

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的<clinit>方法中

静态代码块执行

实际上,static块的执行发生在“初始化”的阶段。初始化阶段,JVM主要完成对静态变量的初始化,静态块执行等工作。

下面我们看看执行static块的几种情况:

  1. 第一次new A()的过程会打印"";因为这个过程包括了初始化
  2. 第一次Class.forName("A")的过程会打印"";因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader());
  3. 第一次Class.forName("A",false,this.getClass().getClassLoader())的过程则不会打印""。因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。

为什么普通代码块优于构造方法执行

几套笔试题下来,终于搞清楚了Java静态代码块-普通代码块-构造方法的执行顺序

通过查看编译完成的 .class文件 发现,普通代码块实际上是被放到了构造方法中,且是放在了构造方法的第一句

那么就不难解释:为什么普通代码块会比构造方法执行顺序考前了。

在执行子类的构造方法时,子类 super 指向父类的构造方法会先执行父类的构造方法。由于静态代码块执行是优先于main方法的,所以先回去执行父类的静态代码块再执行子类的静态代码块。此时 super 由于指向父类需要去执行父类的构造方法,且普通代码块会被转换到构造方法的第一行,所以此时就会执行父类的代码块以及构造方法。当super执行完毕回到子类时,由于子类的代码块也被放到了构造方法中,且在super之后所以执行子类代码块再执行子类构造方法。

因为 main 方法池Java程序执行的入口,所以优于构造方法和普通代码块的执行;但是,静态块是在类加载时的初始化阶段就执行了,此时 main方法还没有进入到虚拟机栈中,所以是静态代码块最先执行。

【文章参考】

[1]  [美] 文纳斯. 《深入Java虚拟机》第二版

[2] oracle docs. https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#forName%28java.lang.String,%20boolean,%20java.lang.ClassLoader%29

[3] oracle docs. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html