JVM

读Java 虚拟机类加载引发的血案

2019-07-21  本文已影响0人  花前月下的细说

最近在看 Java 虚拟机类加载的知识点,结果让我发现了自己一个曾经一直纠结,又没彻底弄懂的类加载黑洞,从而引发下面一系列的测试血案

相信面试过的你们也会见过类似下面测试的这几道题

不过,答案你真的理解了么?

话不多说,直接 GKD

相关学习巨佬博

站在巨佬的肩膀上

https://www.jianshu.com/p/b6547abd0706

https://www.jianshu.com/p/8c8d6cba1f8e

https://www.cnblogs.com/wcd144140/p/7273974.html

看完,大佬们应该都全部理解透了...可惜我不是大佬,所以...哈哈哈 GKD 吧

下面就是测试过程种发现的一些疑惑点,赶紧记录一波.....哎,难顶

测试开始

先思考下下面代码输出什么


class Singleton {

    public Singleton() {
        System.out.println("Singleton new instance");
    }

    static {
        System.out.println("Singleton static block");
    }

    {
        System.out.println("Singleton  block !!!");
    }

}

public class NewTest {
    public static void main(String args[]){
        Singleton singleton = new Singleton();
    }
}

输出结果

Singleton static block
Singleton  block !!!
Singleton new instance


当然,大佬们应该都能知道答案...毕竟,新手入门级的野怪,谁都打得过

这个对我这小菜鸡也算还比较容易理解:

    加载连接过程,没有需要处理的 static 

    new Singleton() 直接开始类的初始化了,所以输出直接按照类的初始化顺序来就好了

类的初始化的执行顺序

没有父类的情况:

1)类的静态属性
2)类的静态代码块
3)类的非静态属性
4)类的非静态代码块
5)构造方法


有父类的情况:

1)父类的静态属性
2)父类的静态代码块
3)子类的静态属性
4)子类的静态代码块
5)父类的非静态属性
6)父类的非静态代码块
7)父类构造方法
8)子类非静态属性
9)子类非静态代码块
10)子类构造方法


这里有个小误区,是我自己的误区~~

比如下面这个例子

class ParentSingleton{
    
public static int value = 100;
    
public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}
    
static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}
    
}

当要初始化上面这个类的时候,会输出什么

如果这时候,我们只看上面的初始化顺序,会觉得这样输出,根据顺序来嘛

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance

???

OMG,错了,这里的顺序不是说,只要初始化,就要全部按照顺序一一执行。。不是这样的

实际上只会输出

ParentSingleton static block

如果有 创建这个类的实例,比如 new ParentSingleton()

才会

ParentSingleton  block !!! 
ParentSingleton new instance

是的,这里的误区,我曾经一度搞错了。。。尴尬

那再看这个测试

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static void forTest() {
    
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton.forTest();
}
}

看完资料的我,逐渐膨胀,毕竟100多斤的胖子,我想的输出应该是

Singleton static block
Singleton  block !!!
Singleton new instance

然后运行一看,懵逼了,结果是

Singleton  block !!! 
Singleton new instance
Singleton static block

咋回事啊,小老弟,结果乱套了...为什么不是先执行 static 代码块先了

认真想了一波,也不知道对不对,只能疯狂测试这样子...

经过一番测试,查看资料...最终...我觉得是这样子的



整个的流程详解:

执行的第一步:Singleton.forTest();

这时候,对Singleton类进行加载和连接,所以首先需要对它进行加载和连接操作。

在连接-准备阶段,要讲给静态变量赋予默认初始值,这里还没到执行 forTest

初始值是   singleton = null

加载和连接完毕之后,再进行初始化工作

private static Singleton singleton = new Singleton();

所以执行去到了 new Singleton();  这里因为 new 会引起 Singleton 的初始化

需要执行 Singleton构造函数里面的内容

但是又因为非static初始化块,这里面的代码在创建java对象实例时执行,而且在构造器之前!!!!就是这东西。。


所以输出应该是

Singleton  block !!! 
Singleton new instance

而根据类的初始化顺序,要执行 static 代码块,应该输出
Singleton static block

完成初始化后

接下来就到真正调用 forTest 方法了,方法什么都不做,没输出


所以,总的答案就是

Singleton  block !!! 
Singleton new instance
Singleton static block



这里最大的原因就是,连接加载的时候,要给属性初始化,而这里的初始化又刚好是 创建java 实例,需要执行构造,执行构造的前面又必须先执行 {} 大括号非 static 块

而不是和第一个测试例子那样,static 属性不需要初始化,所以....

IG 永不加班,但我需要哇 继续测试吧...

继续测试验证

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return new Singleton();
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

输出结果

emm, 再次根据上面自己的理解,走一遍

应该是

Singleton  block !!! 
Singleton new instance
Singleton static block
Singleton  block !!! 
Singleton new instance


这里后面第二次 new 为啥不引起第二次 类的初始化?? 因为一个类只能初始化一次啊

new 只是创建实例,不再初始化了

所以在调用 getSingleton 的时候,只创建实例就好了,而创建实例就是

Singleton  block !!! 
Singleton new instance


在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.

为什么只初始化一次呢?类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到的那个唯一的

噢? 运行看一手,丢 对了。。。。

还有 存在 final 的时候,和存在父类的时候,下面慢慢再测试验证....

继续测试

class Singleton extends ParentSingleton {

public Singleton() {
    System.out.println("Singleton new instance");
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{
    
public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}
    
static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}
    
}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = new Singleton();
}
}

输出结果

这个,很明了,还是按照上面的类的初始化,有父类的情况按顺序调用,输出


ParentSingleton static block
Singleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance

继续测试

那个人 又来了。。。改成和上面没有父类一样的情况

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{
    
public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}
    
static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}
    
}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

输出结果

这里,就开始懵了。。。有点


先看结果


ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block


其实,很容易看清了,现在,再走一遍流程吧

执行到 Singleton.getSingleton() 时

先加载 Singleton  ,这时因为 Singleton 有父类,需要需要加载父类先

加载父类 ParentSingleton,根据加载流程,在连接-准备阶段,要讲给静态变量赋予默认初始值,但父类没有 static 属性需要赋值初始化什么的,但是根据顺序,需要初始化static 代码块

ParentSingleton static block


这时候回到 子类的 加载流程

根据连接-准备阶段,子类有需要处理的属性 private static Singleton singleton = new Singleton()

赋值默认值先,singleton = null

然后初始化 singleton = new Singleton()

根据上面的经验,这里是创建实例 ,并引起初始化,正常应该是

Singleton  block !!! 
Singleton new instance
Singleton static block

但是,重点来了 !!

类实例创建过程:按照父子继承关系进行初始化,首先执行父类的初始化块部分
然后是父类的构造方法;再执行本类继承的子类的初始化块,最后是子类的构造方法

也就是

ParentSingleton  block !!! 
ParentSingleton new instance


同时子类的初始化,因为初始化子类它有父类,所以需要先初始化父类(但是这里因为父类已经初始化了,就不再初始化了)


所以结果是:

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

最终测试

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{
    
private static ParentSingleton parentSingleton = new ParentSingleton();   
    
public ParentSingleton(){
    
    System.out.println("ParentSingleton new instance");
}
    
static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}
    
}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

测试结果

ParentSingleton  block !!! 
ParentSingleton new instance
ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block


加载一个类时,先加载父类


按照先加载,创建实例,初始化,这个顺序就发现 很通顺的写出答案了


哈哈哈哈哈,终于清楚了 所以一切的一切,都是创建实例这个东西。。搞得我头晕

部分特殊不引起类初始化记录

先记录下吧

1. 通过子类引用父类的静态字段,不会导致子类初始化,对于静态字段,只有直接定义这个字段的类才会被初始化
2. 通过数组定义来引用类,不会触发此类的初始化
3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
4. public static final int x =6/3;  能够在编译时期确定的,叫做编译常量,不会引起类的初始化!!!
5. public static final int x =new Random().nextInt(100); 运行时才能确定下来的,叫做运行时常量,运行常量会引起类的初始化!!!

引起类初始化记录

在虚拟机规范中使用了一个很强烈的限定语:“有且仅有”,这5种场景中的行为称为对类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用

    5种必须初始化的场景如下

1.  遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要先触发其初始化

    这4条指令对应的的常见场景分别是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
    
注:静态内容是跟类关联的而不是类的对象

2.  使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化

注:反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法
    
    对于任意一个对象,都能够调用它的任意一个方法和属性
    
    这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制,这相对好理解为什么需要初始化类
    
3.  当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

注:子类执行构造函数前需先执行父类构造函数

4.  当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

注:main方法是程序的执行入口

5.  当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。则需要先触发其初始化

注:JDK1.7的一种新增的反射机制,都是对类的一种动态操作

这回,以后看代码的时候,就不会再被这些执行加载顺序弄混了,对优化代码可能还是有帮助的吧

再不说,也能再让我看到这些测试题,或者问我加载的过程,怎么也能处理回答个7788了吧

可能其中个人理解有部分纰漏,还请大佬们指出~~蟹蟹鸭

后面还要去验证测试下面这些情况

1.  子类引用指向父类
2.  ...等等

任重而道远,我是桥豆麻袋,下回再见~

上一篇下一篇

猜你喜欢

热点阅读