大家都知道一个Java类从class文件到可以运行时,需要经历:加载、链接、初始化等过程,然后才能被创建对象(实例化)。
今天问一个有趣的问题,有可能一个对象的实例化调用,要早于其初始化吗?
白话点说:这个类中的构造方法要早于静态代码块的执行?
- 怎么写会出现这种情况?
- 分析其执行流程
更多问答 >>
-
每日一问 | 今天还探索一个 View 的方法 hasOverlappingRendering()
2021-02-21 20:16 -
2021-03-18 23:20
-
每日一问 | 在做性能优化的时候,常常看到 Thread(Cpu) Time,Wall clock Time?
2021-03-15 00:43 -
每日一问 | onDraw 里面调用 invalidate 做动画,有什么问题?
2021-04-13 00:31 -
每日一问 | mipmap vs drawable,傻傻分不清楚?
2021-03-30 21:14 -
2021-01-31 16:58
-
每日一问 | Android 中两种设置线程优先级的方式,有何区别?
2021-01-27 23:59 -
每日一问 | Java 中的 lambda 与 Android 中的 lambda 有什么不同?
2021-01-31 17:20 -
每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?
2021-01-11 00:00 -
每日一问 | RxJava中Observable、Flowable、Single、Maybe 有何区别?
2021-01-03 20:34
上周公众号也有推过这个问题: https://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650835380&idx=2&sn=0acfb263d4a56987cc521bbfa4467518
文章说:如果在类里面声明一个类型为它自身的静态字段并实例化,像这样:那么在运行时会看到initialization block和constructor先于static initialization block被执行:
探究下为什么
先看看字节码:这里把【实例化
再转成java代码,实际等同于这样:INSTANCE
】和【打印static initialization block invoked
】的指令都合并到一起了。还有这样:
可以看到
一个类的【静态初始化块】只有在类被加载时才会执行且只有一次像上面这种情况,如果在INSTANCE
的实例化是处于【静态初始化块】里面的,结合我们所学的Java基础:INSTANCE
实例化之前还执行【静态初始化块】的话,岂不是一直递归下去了?所以我猜测,JVM在即将执行【静态初始化块】之前,会有个FLAG记录当前状态,比如:是否已经初始化过、是否处于初始化中。只有在这两个状态都没有标记过的情况下,才会执行【静态初始化块】的代码。把JDK源码(https://github.com/openjdk/jdk/)下载下来,印证一下这个猜想:
只是,源码那么庞大,应该怎么快速定位到具体文件呢?不知道大家有没有了解过这个Error:ExceptionInInitializerError,如果在类的【静态初始化块】执行过程中发生没有捕获的异常,就会抛出这个错。emmmm,我们可以尝试把它作为关键词搜一下,如果有的话,应该离【调用静态初始化块】的代码也不远了:grep -rn "ExceptionInInitializerError" | grep /vm/ | grep ExceptionInInitializerError
(因为我们要看JVM的代码,所以加了/vm/目录过滤)咦?这个叫instanceKlass.cpp的文件,有点可疑,打开看下:
instanceKlass中,有ExceptionInInitializerError字眼的都集中在一个else里面,前面两处都是注释,第三处看样子是创建了java的ExceptionInInitializerError对象并抛出。
往上看:原来是在一个叫
那基本可以确定Class的初始化就是这里了,从头看一遍(注意看注释):initialize_impl
的函数里面,咦?看这个函数第一行的注释:在初始化之前确保Class已经链接?!接着又调用了link_class
函数。!!!真的就是我们所想的那样,有个正在初始化的状态!
如果在【静态初始化块】中创建自身实例的话,因为Class还没初始化完毕,最终还是会走到这个函数里,但是在继续调用【静态初始化块】代码之前,检测到当前Class正在初始化中,所以本次就直接跳过了这个流程!整理一下这句话:在类的【静态初始化块】中创建自身实例,会看到构造方法的log先于【静态初始化块】的log打印,这是因为在JVM在初始化类时,会检查目标Class是否处于is_initialized(已经初始化)或being_initialized(正在初始化)状态,如果是的话,会直接忽略,因为要保证【静态初始化块】只执行一次。emmmm,在开头公众号的文章里,还提到一个问题:
多线程进行类的初始化会出问题吗文章说不会(因为是线程安全的),但实际上会有个安全隐患。。。。那就是。。。死锁!!!从上面的JVM源码能看出来,当Class初始化时,同时间只能有一个线程通过,其他线程会一直等待,所以就给了死锁机会(当然了这是很极端的情况才会发生)。可以写代码来测试下:哈哈哈哈哈,编译运行以上代码会发现,"Done."永远不会打印,进程一直卡在那里。
自惭形秽。。。
你的jdk源码是啥版本的,和我下的有些区别
我的是openjdk8 <https://gitee.com/ckl111/openjdk-jdk8u>,以前下载的了,只是昨天才发现github上也有。github上的是一直在更新的,不 ...查看更多
我的是openjdk8 <https://gitee.com/ckl111/openjdk-jdk8u>,以前下载的了,只是昨天才发现github上也有。github上的是一直在更新的,不过我刚刚对比了下instanceKlass.cpp,虽然改动挺多但大致逻辑没变
你再顺着那个initialize_impl函数往上找,会看到它在ClassLoader,Unsafe,new,MethodHandle这些都有间接或直接的调用
1.哈哈哈,被我发现了:
为什么构造方法找于静态代码块执行了呢?看字节码很容易看出原因,其的类构造方法(注意,不是对象构造方法)如下:
从上面可以看到,类构造方法的调用是,先初始化了instance对象,然后才调用的静态代码块里面的内容。
2.现在我们试着分析下其的执行流程,这个咋一看,有点像鸡(类)还没有出生,蛋(对象)就已经被创建了,但是我们可以先想下,一个对象被创建出来的先决条件是什么?对象是什么?
抛开虚拟机,其实对象也只是在内存里的一些数据,然后根据类的结构,我们能够从这些数据里读取出这个对象的一个个field,然后,一个对象如果要被创建出来,其实只有一个条件:确认这个对象所占的内存,然后再遇到new指令的时候在内存里分配好指定的内存就好了。
然后我们回到这个题目,当我们调用了CInitTest.a时,虚拟机发现类没有被初始化,然后就先调用累的加载,也就是把文件形式的class加载到内存,然后做类的连接,第一步先校验,也就是校验类的文件结构对不对(不可能你随便放个文件后缀改成class都能被加载的不是吗),第二步是准备,也就是分配静态变量的内存空间,对于本例,会分配a以及instance 的内存,犹豫他们是对象的引用,所以占的内存空间是确定的(32位或者64位),第三部是解析,也就是将类中的符号引用转换为直接引用,可以理解为,在class里,字段方法等的引用是表示为存在class文件的字符串,这一步将其转化为真正的内存地址。
上面这些是虚拟机调用的,严格的顺序,我们干涩不了,然后就是类的初始化了,也就是调用 <clinit>()V方法,然后这一步的时候,对于本例而言,先将空字符串的引用赋给静态变量a,然后调用new指令,在堆里分配一块内存,由于类的结构已经被分析完了,这一步是可以做的,然后调用对象构造方法,构造方法也是一个方法,其已经被解析了,调用完成之后,对象就被构造出来了,然后把引用(也就是对象在堆里的地址)赋给静态变量instance ,最后调用静态代码块,整个流程完结。
最好,推荐大家读读深入理解java虚拟机这本书,很不错,对方法调用锁机制对象等会有更深入的理解