从虚拟机的角度理解静态代码块和构造代码块
所谓静态代码块,是指用static关键字修饰的代码块,特点是代码块会在类的构造代码块、构造函数之前运行, 且只会执行一次。而构造代码块,则就是单纯的由花括号构成的代码块,特点是代码块会在类的构造函数之前运行, 且每次实例化对象都会被调用。本篇blog从虚拟机的角度描述静态代码块和构造代码块,加深理解。
首先,我们要知道,当你将.java文件编译成.class文件时,如果有静态代码块的话, 他会在.class文件中插入一段称为<clinit>
的函数代替静态代码块。如果有构造代码块,他会在各个构造函数中插入一段称为<init>
的函数代替构造代码块
其次,我们要理解类的生命周期(如下图),<clinit>
函数的调用发生在初始化阶段,而初始化阶段仅有一次,所以
<clinit>
函数仅会调用一次,这就是为什么 静态代码块仅会在“使用类”的时候被调用一次,其他情况均不会被调用。
咱们使用javap反汇编看看。
public class Student { static { System.out.println("第一个构造代码块"); } static { System.out.println("第二个构造代码块"); } { System.out.println("第一个构造代码块"); } { System.out.println("第二个构造代码块"); } public Student() { System.out.println("默认构造函数"); } public Student(String name,int age) { System.out.println("自定义构造函数"); this.name = name; this.age = age; } private String name = "Clive"; private static int age = 17; private final static String COUNTRY = "CN"; private final String SCHOOL = "JXAU"; } /* E:\eclipseWork\Test\bin\test>javap -c Student 警告: 二进制文件Student包含test.Student Compiled from "Student.java" public class test.Student { static {}; Code: 0: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #25 // String 第一个构造代码块 5: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #33 // String 第二个构造代码块 13: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: bipush 17 18: putstatic #35 // Field age:I 21: return public test.Student(); Code: 0: aload_0 1: invokespecial #40 // Method java/lang/Object."<init>":()V 4: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #25 // String 第一个构造代码块 9: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #33 // String 第二个构造代码块 17: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_0 21: ldc #42 // String Clive 23: putfield #44 // Field name:Ljava/lang/String; 26: aload_0 27: ldc #14 // String JXAU 29: putfield #46 // Field SCHOOL:Ljava/lang/String; 32: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 35: ldc #48 // String 默认构造函数 37: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: return public test.Student(java.lang.String, int); Code: 0: aload_0 1: invokespecial #40 // Method java/lang/Object."<init>":()V 4: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #25 // String 第一个构造代码块 9: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #33 // String 第二个构造代码块 17: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_0 21: ldc #42 // String Clive 23: putfield #44 // Field name:Ljava/lang/String; 26: aload_0 27: ldc #14 // String JXAU 29: putfield #46 // Field SCHOOL:Ljava/lang/String; 32: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 35: ldc #53 // String 自定义构造函数 37: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: aload_0 41: aload_1 42: putfield #44 // Field name:Ljava/lang/String; 45: iload_2 46: putstatic #35 // Field age:I 49: return } */
咦?说好的<clinit>
函数呢?这是因为<clinit>
函数不能被java调用,它只能作为类加载过程的一部分由JVM直接调用,具体见文末引用。JVM对函数做了“美化处理”, 反汇编的代码中static {};
就是指<clinit>
静态代码块。从<clinit>
函数中,我们可以得到一些信息。
- 多个静态代码块最终会融合成一个
<clinit>
函数供JVM调用
源码中有两个静态代码块,而反汇编得出的代码仅有一个<clinit>
- 静态数据域的赋值操作在
<clinit>
函数中完成
数据域age是静态数据域(private static int age = 17;),对应<clinit>
函数中偏移量为18的指定。
18: putstatic #28 // Field age:I
从构造函数中,我们可以得到与构造代码块相关的一些信息
- final static数据域的赋值操作不在
<clinit>
函数中完成
数据域COUNTRY用了不仅用了static关键字修饰,还用了final修饰,但是反汇编代码中并没有相关的赋值指令, 因为当一个数据域用final修饰时,其赋值操作在准备阶段就已经完成了,数据域SCHOOL也是如此。
- 每当构造函数运行时,<init>函数都会被调用
从反汇编代码中可以看到,默认构造函数和自定义构造函数中都插入了一段调用<init>
函数的字节码指令, invokespecial #33 // Method java/lang/Object."":()V 这也就是为什么构造代码块有这个特性:每次实例化对象时静态代码块都会被执行,且在构造函数之前执行
- 多个构造代码块也被融合成了一个函数
源码中有两个构造代码块,而构造函数中仅调用了一次<init>
函数,可见虚拟机将构造代码快融合了
- 普通数据域的赋值操作在构造函数中完成
数据域name是普通的数据域(private String name = "Clive";)。赋值操作在默认构造函数中对应的是 偏移量为21的字节码指令,在自定义构造函数中,对应的字节码偏移量也为21。
静态代码块与<clinit>
的关系,构造代码块与<init>
的关系
根据上面的反汇编代码,我们得知,把<clinit>
函数简单的当做静态代码块是错误的一个观点,因为<clinit>
函数中还包含了 对静态数据域的赋值操作,所以,<clinit>
函数与静态代码块的关系应该是包含关系。构造代码块与<init>
的关系也是如此,不再赘述。
继承关系中的
当父类中也有静态代码块时,子类的静态代码块中并没有显示或者隐式调用父类静态代码块的指令,但虚拟机会保证在子类的<clinit>
函数运行之前,父类的<clinit>
已经运行完成。
并非所有类都会产生函数
如果类中没有静态代码块,也没有静态数据域的赋值操作,也就不会产出<clinit>
函数
public class Student { { System.out.println("第一个构造代码块"); } { System.out.println("第二个构造代码块"); } public Student() { System.out.println("默认构造函数"); } public Student(String name,int age) { System.out.println("自定义构造函数"); this.name = name; } private String name = "Clive"; private final static String COUNTRY = "CN"; private final String SCHOOL = "JXAU"; } /* E:\eclipseWork\Test\bin\test>javap -c Student 警告: 二进制文件Student包含test.Student Compiled from "Student.java" public class test.Student { public test.Student(); Code: 0: aload_0 1: invokespecial #17 // Method java/lang/Object."<init>":()V 4: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #25 // String 第一个构造代码块 9: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #33 // String 第二个构造代码块 17: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_0 21: ldc #35 // String Clive 23: putfield #37 // Field name:Ljava/lang/String; 26: aload_0 27: ldc #12 // String JXAU 29: putfield #39 // Field SCHOOL:Ljava/lang/String; 32: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 35: ldc #41 // String 默认构造函数 37: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: return public test.Student(java.lang.String, int); Code: 0: aload_0 1: invokespecial #17 // Method java/lang/Object."<init>":()V 4: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #25 // String 第一个构造代码块 9: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #33 // String 第二个构造代码块 17: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_0 21: ldc #35 // String Clive 23: putfield #37 // Field name:Ljava/lang/String; 26: aload_0 27: ldc #12 // String JXAU 29: putfield #39 // Field SCHOOL:Ljava/lang/String; 32: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 35: ldc #48 // String 自定义构造函数 37: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: aload_0 41: aload_1 42: putfield #37 // Field name:Ljava/lang/String; 45: return } */
把Student中静态代码块和静态数据域删掉,最终生成的反汇编代码中并没有<clinit>函数
接口也会产生<clinit>函数
接口中是不可以写静态代码块的(The interface XXX cannot define an initializer),但其数据域都默认是静态的, 所以也会产生<clinit>
函数。另外,只有真正使用了这个接口,其<clinit>
函数才会运行。也就是说,两个接口A、B,A是B的父接口, 当使用B接口时,A接口的<clinit>
函数并不会用运行,只有真正使用了A接口,其<clinit>
函数才会运行,比如使用接口中的数据域。 另外,我不想骗各位,书上大意是如此,但是,我用javap生成的反汇编代码发现,并没有生成<clinit>
函数,而是生成了一个默认构造函数,里面有对数据域的赋值操作,我想可能是JDK版本不同吧 :)
public class Speak { int COUNT = 2; } /* E:\eclipseWork\Test\bin\test>javap -c Speak 警告: 二进制文件Speak包含test.Speak Compiled from "Speak.java" public class test.Speak { int COUNT; public test.Speak(); Code: 0: aload_0 1: invokespecial #10 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_2 6: putfield #12 // Field COUNT:I 9: return } */
<clinit>
函数的线程安全性
虚拟机内部,为确保其线程安全,当多线程视图初始化同一个类时,仅一条线程允许进入<clinit>
函数,其他线程必须等待。当<clinit>
函数 执行完成时,其他线程发现<clinit>
函数已经执行完毕,也就不会在执行<clinit>
函数了。这种互斥的同步操作使用锁完成的,有锁就 有可能导致死锁,如下。
package test; import java.sql.SQLException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) throws ClassNotFoundException, SQLException { // 模仿多线程情况下初始化类 // ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(new Task("test.A")); executor.execute(new Task("test.B")); executor.shutdown(); System.out.println("over"); } private static class Task implements Runnable { private String className; public Task(String className) { this.className = className; } public void run() { try { Class.forName(className); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
package test; public class A { static { System.out.println("enter static block of A"); try { Thread.sleep(5000); Class.forName("test.B"); //初始化B } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("out static block of A"); } }
package test; public class B { static { System.out.println("enter static block of B"); try { Thread.sleep(5000); Class.forName("test.A"); //初始化A } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("out static block of B"); } }
上述代码模拟了在多线程环境下初始化类而造成的死锁现象。当一个线程进入类A的<clinit>
函数时, 根据代码,其又要初始化类B,而类B的<clinit>
函数已经由另一个线程占据,且要初始化类A。这样双方都抱着自己的资源不放,又去请求别的资源,自然会造成死锁。
总结
<clinit>函数包含了静态代码块中的代码,在类的初始化阶段运行,且仅会运行一次。<init>包含了构造代码块中的代码,在实例化对象的时候运行,会在构造函数之前运行
引用
1.《深入理解Java虚拟机》
2.《实战Java虚拟机》
3.论坛:http://hllvm.group.iteye.com/group/topic/35224