Java 类初始化与this逃逸
前言
想记录一下对类初始化的理解,并且this逃逸也与类初始化有关,所以放到一起了。
类的初始化
先看一段代码,并想想它的运行结果是什么?
public class StaticTest { public static void main(String[] args) { staticFunction(); } static StaticTest st = new StaticTest(); static { System.out.println("1"); } { System.out.println("2"); } StaticTest() { System.out.println("3"); System.out.println("a = " + a + ", b = " + b); } public static void staticFunction() { System.out.println("4"); } int a = 10; static int b = 112; }
运行结果:
2
3
a = 10, b = 0
1
4
是不是如你所想呢?
不管是不是也说说出现这个结果的步骤吧
静态域先于实例域初始化这个是大家都知道的,但是为什么上面的输出中看到实例域的代码反而先运行了呢?
关键代码:
static StaticTest st = new StaticTest();
静态域的第一行执行了构造方法,在执行main方法时,先是加载这个类,加载完毕后紧接着初始化,初始化则是初始化静态域,实例域是赋0值(不同的类型0值不同),但因为这里调用了构造方法,就开始执行实例化的流程,初始化实例域的顺序是按照代码在文件中出现的顺序来的同时又先于构造器(这是为什么等会说),所以先是输出2,然后执行a=10,输出3和"a=10, b = 0"但是为什么b=0呢,因为静态域也是按照先出现先执行的原则进行执行的,最后执行方法输出4。
通过字节码文件可以更清晰的看懂初始化流程。
public static void main(java.lang.String[]); Code: 0: invokestatic #1 // Method staticFunction:()V 3: return mytest.inittest.StaticTest(); Code: 0: aload_0 1: invokespecial #2 // Method java/lang/Object."<init>":()V 4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #4 // String 2 9: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_0 13: bipush 10 15: putfield #6 // Field a:I 18: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 21: ldc #7 // String 3 23: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 26: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 29: new #8 // class java/lang/StringBuilder 32: dup 33: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V 36: ldc #10 // String a = 38: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 41: aload_0 42: getfield #6 // Field a:I 45: invokevirtual #12 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 48: ldc #13 // String , b = 50: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 53: getstatic #14 // Field b:I 56: invokevirtual #12 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 59: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 62: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 65: return public static void staticFunction(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #16 // String 4 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return static {}; Code: 0: new #17 // class mytest/inittest/StaticTest 3: dup 4: invokespecial #18 // Method "<init>":()V 7: putstatic #19 // Field st:Lmytest/inittest/StaticTest; 10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 13: ldc #20 // String 1 15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: bipush 112 20: putstatic #14 // Field b:I 23: return
通过字节码可以清楚的看到,之前的类文件被分成了四个部分,分别是2个方法和构造函数和一个静态域,这也是之前说的实例域的代码会在构造器前执行同时按照出现的顺序排列,静态域也是一样,是根据字节码文件下的结论。毕竟字节码文件是严格按照虚拟机规范的产物。
从字节码文件看到static这部分,
- 调用构造函数,
- 打印1,
- 给b赋值
构造函数部分
- 打印2
- 给a赋值
- 打印a=10, b=0
new的时候就跳到构造函数了,也就是StaticTest(),这里把散落在构造函数外的代码块和成员赋值的代码都收集并放到构造函数原来代码的前面了(先于构造函数执行,并且如果在实例域调用构造函数是会出现栈溢出的)。所以实际上还是按照先静态再实例的初始化顺序执行的。
This逃逸
this逃逸说的是在初始化还未完全时将this所指向的地址传递出去,使得其它线程可以访问到未初始化完全的实例。也就是说它发生在并发场景下,想来也是,单线程大家都是顺序执行,哪来逃逸一说。
new不是原子操作可以说是其中一个原因,new操作分为下面三步:
- 分配内存空间
- 初始化对象
- 将声明的变量指向此实例地址
但是经过jvm的指令重排后三步变化了:
- 分配内存空间
- 将声明的变量指向实例地址
- 初始化对象
2和3的交换是导致this逸出的根本原因。
下面是代码示例:
public class ThisEscapeTest { private String status; private String message; public ThisEscapeTest(Notifier notifier) throws InterruptedException { status = "new"; notifier.registerListener((Event event) -> { // 输出的结果并不是在构造函数中打印的,也就是说不是在主线程中打印的 System.out.println("current thread: " + Thread.currentThread().getName()); System.out.println("status: " + status); System.out.println("message: " + message); }); // 模拟耗时操作,导致初始化结束前其它线程访问了此内存空间 Thread.sleep(2000); message = "message"; } public static void main(String[] args) throws InterruptedException { Notifier notifier = new Notifier(); new Thread(() -> { try { // 这里发生某些事件,导致通知者通知监听者 notifier.notifyListeners(); } catch (InterruptedException e) { e.printStackTrace(); } }, "A").start(); new ThisEscapeTest(notifier); } // 在示例中没有实际用处,模拟正常情况的监听事件 static class Event { } // 通知者,注册监听者和在出现事件时通知已注册的监听者 static class Notifier { ArrayList<EvenListener> listeners = new ArrayList<>(); synchronized void registerListener(EvenListener listener) { listeners.add(listener); this.notifyAll(); } synchronized void notifyListeners() throws InterruptedException { while (listeners.size() <= 0) { this.wait(); } // 输出结果是在这里打印的 for (EvenListener listener : listeners) { listener.doSomething(new Event()); } } } // 监听者接口 interface EvenListener { void doSomething(Event event); } }
输出结果:
current thread: A
status: new
message: null
message为null说明初始化未完全已被其它线程访问。