目录
验证
为什么静态块先执行,在什么时候执行
JVM类加载过程
静态代码块执行
为什么普通代码块优于构造方法执行
误区探究:静态代码块,会在类被加载时自动执行吗
【结论】
静态代码块 ==> main方法 ==> 普通代码块 ==> 构造方法
- 如果有继承关系,父类先由于子类执行
- 静态代码块只在类加载时执行一次(后面再实例化对象时,该类已经加载过,所以静态块不再执行)
验证
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();
}
}
执行结果:
- 父类静态代码块
- 子类静态代码块
- 子类main方法执行
- 父类非静态代码块
- 父类构造方法
- 子类非静态代码块
- 子类构造方法
- 父类普通方法执行
- ------------------------ 第二次实例化对象,父类和子类的静态代码块不再执行
- 父类非静态代码块
- 父类构造方法
- 子类非静态代码块
- 子类构造方法
- 父类普通方法执行
为什么静态块先执行,在什么时候执行
JVM类加载过程
首先,得从JVM类加载器说起。可以参考我之前写过的文章
🔗【探究JVM一】JVM的入口——类加载器子系统
一个类从 .java 到 .class 加载到JVM解释执行,需要经历三大步骤:
加载 ==> 链接 ==> 初始化
其中装载阶段又三个基本动作组成:
- 通过类型的完全限定名,产生一个代表该类型的二进制数据流
- 解析这个二进制数据流为方法区内的内部数据结
- 构创建一个表示该类型的java.lang.Class类的实例
另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。
链接阶段又分为三部分:
- 验证,确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),另外还需要进行符号引用的验证。
- 准备,Java虚拟机为类变量分配内存,设置默认初始值。
- 解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
当一个类被主动使用时,JVM就会对其初始化,如下六种情况为主动使用:
- 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
- 当调用某个类的静态方法时
- 当使用某个类或接口的静态字段时
- 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
- 当初始化某个子类时
- 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
-
JDK1.7开始提供的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始
被动使用
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的<clinit>方法中
静态代码块执行
实际上,static块的执行发生在“初始化”的阶段。初始化阶段,JVM主要完成对静态变量的初始化,静态块执行等工作。
下面我们看看执行static块的几种情况:
- 第一次new A()的过程会打印"";因为这个过程包括了初始化
- 第一次Class.forName("A")的过程会打印"";因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader());
- 第一次Class.forName("A",false,this.getClass().getClassLoader())的过程则不会打印""。因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。
为什么普通代码块优于构造方法执行
通过查看编译完成的 .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