java 虚拟机 代码块_从虚拟机的角度理解静态代码块和构造代码块

所谓静态代码块,是指用static关键字修饰的代码块,特点是代码块会在类的构造代码块、构造函数之前运行, 且只会执行一次。而构造代码块,则就是单纯的由花括号构成的代码块,特点是代码块会在类的构造函数之前运行, 且每次实例化对象都会被调用。本篇blog从虚拟机的角度描述静态代码块和构造代码块,加深理解。

首先,我们要知道,当你将.java文件编译成.class文件时,如果有静态代码块的话, 他会在.class文件中插入一段称为的函数代替静态代码块。如果有构造代码块,他会在各个构造函数中插入一段称为的函数代替构造代码块

其次,我们要理解类的生命周期(如下图),函数的调用发生在初始化阶段,而初始化阶段仅有一次,所以函数仅会调用一次,这就是为什么 静态代码块仅会在“使用类”的时候被调用一次,其他情况均不会被调用。

e52461bdaec20201b20772999672c2e1.png

咱们使用javap反汇编看看。

public classStudent {static{

System.out.println("第一个构造代码块");

}static{

System.out.println("第二个构造代码块");

}

{

System.out.println("第一个构造代码块");

}

{

System.out.println("第二个构造代码块");

}publicStudent() {

System.out.println("默认构造函数");

}public Student(String name,intage) {

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."":()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."":()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

}*/

咦?说好的函数呢?这是因为函数不能被java调用,它只能作为类加载过程的一部分由JVM直接调用,具体见文末引用。JVM对函数做了“美化处理”, 反汇编的代码中static {};就是指静态代码块。从函数中,我们可以得到一些信息。

多个静态代码块最终会融合成一个函数供JVM调用

源码中有两个静态代码块,而反汇编得出的代码仅有一个

静态数据域的赋值操作在函数中完成

数据域age是静态数据域(private static int age = 17;),对应函数中偏移量为18的指定。

18: putstatic #28 // Field age:I

从构造函数中,我们可以得到与构造代码块相关的一些信息

final static数据域的赋值操作不在函数中完成

数据域COUNTRY用了不仅用了static关键字修饰,还用了final修饰,但是反汇编代码中并没有相关的赋值指令, 因为当一个数据域用final修饰时,其赋值操作在准备阶段就已经完成了,数据域SCHOOL也是如此。

每当构造函数运行时,函数都会被调用

从反汇编代码中可以看到,默认构造函数和自定义构造函数中都插入了一段调用函数的字节码指令, invokespecial #33 // Method java/lang/Object."":()V 这也就是为什么构造代码块有这个特性:每次实例化对象时静态代码块都会被执行,且在构造函数之前执行

多个构造代码块也被融合成了一个函数

源码中有两个构造代码块,而构造函数中仅调用了一次函数,可见虚拟机将构造代码快融合了

普通数据域的赋值操作在构造函数中完成

数据域name是普通的数据域(private String name = "Clive";)。赋值操作在默认构造函数中对应的是 偏移量为21的字节码指令,在自定义构造函数中,对应的字节码偏移量也为21。

静态代码块与的关系,构造代码块与的关系

根据上面的反汇编代码,我们得知,把函数简单的当做静态代码块是错误的一个观点,因为函数中还包含了 对静态数据域的赋值操作,所以,函数与静态代码块的关系应该是包含关系。构造代码块与的关系也是如此,不再赘述。

继承关系中的

当父类中也有静态代码块时,子类的静态代码块中并没有显示或者隐式调用父类静态代码块的指令,但虚拟机会保证在子类的函数运行之前,父类的已经运行完成。

并非所有类都会产生函数

如果类中没有静态代码块,也没有静态数据域的赋值操作,也就不会产出函数

public classStudent {

{

System.out.println("第一个构造代码块");

}

{

System.out.println("第二个构造代码块");

}publicStudent() {

System.out.println("默认构造函数");

}public Student(String name,intage) {

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."":()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."":()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中静态代码块和静态数据域删掉,最终生成的反汇编代码中并没有函数

接口也会产生函数

接口中是不可以写静态代码块的(The interface XXX cannot define an initializer),但其数据域都默认是静态的, 所以也会产生函数。另外,只有真正使用了这个接口,其函数才会运行。也就是说,两个接口A、B,A是B的父接口, 当使用B接口时,A接口的函数并不会用运行,只有真正使用了A接口,其函数才会运行,比如使用接口中的数据域。 另外,我不想骗各位,书上大意是如此,但是,我用javap生成的反汇编代码发现,并没有生成函数,而是生成了一个默认构造函数,里面有对数据域的赋值操作,我想可能是JDK版本不同吧 :)

public classSpeak {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."":()V

4: aload_0

5: iconst_2

6: putfield #12 // Field COUNT:I

9: return

}*/

函数的线程安全性

虚拟机内部,为确保其线程安全,当多线程视图初始化同一个类时,仅一条线程允许进入函数,其他线程必须等待。当函数 执行完成时,其他线程发现函数已经执行完毕,也就不会在执行函数了。这种互斥的同步操作使用锁完成的,有锁就 有可能导致死锁,如下。

packagetest;importjava.sql.SQLException;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;public classTest {public static void main(String[] args) throwsClassNotFoundException, 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 implementsRunnable {privateString className;publicTask(String className) {this.className =className;

}public voidrun() {try{

Class.forName(className);

}catch(ClassNotFoundException e) {

e.printStackTrace();

}

}

}

}

packagetest;public classA {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");

}

}

packagetest;public classB {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的函数时, 根据代码,其又要初始化类B,而类B的函数已经由另一个线程占据,且要初始化类A。这样双方都抱着自己的资源不放,又去请求别的资源,自然会造成死锁。

总结

函数包含了静态代码块中的代码,在类的初始化阶段运行,且仅会运行一次。包含了构造代码块中的代码,在实例化对象的时候运行,会在构造函数之前运行

引用

1.《深入理解Java虚拟机》

2.《实战Java虚拟机》

3.论坛:http://hllvm.group.iteye.com/group/topic/35224

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值