【JVM】深入理解类加载机制(二)

8 篇文章 0 订阅

1 首先复习下主动使用的7中情况

  • 主动使用
  1. 实例化一个类对象
  2. 访问类/接口的静态变量,或者对其进行赋值操作
  3. 调用类/接口的静态方法
  4. 反射–Class.forName(“com.test.Test”)
  5. 实例化该类的子类
  6. JVM标记为启动类的类(包含main方法的类)
  7. JDK1.7 后新增对动态语言的支持,java.lang.invoke.MethodHandle实例解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则需要初始化。

2 访问类中的静态变量

看第一段代码

package com.cj.jvm.classloader;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str2);
    }
}

class MyParent1{
    public static String str = "hello world";

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

class MyChild1 extends  MyParent1{
    public static String str2 = "welcome";

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

情况1: main函数中打印 MyChlid1.str
结果:
在这里插入图片描述
分析: 符合情况2。访问类/接口的静态变量,或者对其进行赋值操作,该类会进行初始化操作。由于MyChild的str变量实际是继承于MyParent1。所以对MyParent1类进行初始化。

情况2: main函数中打印 MyChlid1.str2
结果:
在这里插入图片描述
分析: 符合情况2和5。首先是访问str2,这是MyChild1中定义的静态变量,访问时需要对MyChild1进行初始化。又由于MyChild1是MyParent1的子类,对MyChild1进行初始化之前需要先让MyParent1初始化。


结论: 对一个静态字段来说,只有直接定义了该字段的类才会被初始化,当一哥类在初始化时,要求其父类全部已经初始化完毕了


3 常量–编译期可知与编译期不可知

看另一个例子

package com.cj.jvm.classloader;

public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(MyChild2.str2);
    }
}

class MyParent2{
    public static final String str = "hello world";
    static {
        System.out.println("MyParent1 static block");
    }
}

class MyChild2 extends  MyParent2{
    public static final String str2 = "welcome";

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

分别在main函数中,打印MyChild2.str和MyChild2.str2,结果如下
在这里插入图片描述
在这里插入图片描述

分析:由于str和str2是字面常量,即在编译的时候已经将他们的信息放在了字节码中。但是这有三个类,放在那个类的字节码中呢?

记住下面这句话:常量在编译阶段,会存入到,调用这个常量的方法,所在的类的,常量池中。

所以在本例子中,str(或者str2)在第一次编译后,存到了MyTest2的常量池中。

查看MyTest2的字节码文件,#4号位置就是了
在这里插入图片描述


上面说的是编译期可知的常量,下面说下,运行时可知的常量,看例子

package com.cj.jvm.classloader;

import java.util.UUID;

public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}

class MyParent3{

    public static final String str = UUID.randomUUID().toString();

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

输出结果:
在这里插入图片描述
分析:依旧是常量,只不过是变成了运行时才可知的常量,这就导致了该常量所在的类被初始化。

结论: 当一个常量的值是在编译期无法确定的,那个它的值就不会被放到调用类的常量池中,在程序运行时,会导致主动使用这个常量所在的类,然后导致该类的初始化


4 类的数组是否会导致类的初始化? – 不会

看个例子

package com.cj.jvm.classloader;

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[1];
        System.out.println(myParent4s.getClass());
        System.out.println(myParent4s.getClass().getSuperclass());

        MyChild4[] myChild4s = new MyChild4[1];
        System.out.println(myChild4s.getClass());
        System.out.println(myChild4s.getClass().getSuperclass());

        int[] ints = new int[1];
        System.out.println(ints.getClass());
        System.out.println(ints.getClass().getSuperclass());


    }
}

class MyParent4{
    static {
        System.out.println("MyParent4 static block");
    }
}
class MyChild4 extends MyParent4{

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

运行结果:
在这里插入图片描述
分析:是的,对于数组实例来说,其类型是有JVM在运行期间动态生成的。表示为[Lcom.cj.jvm.classloader.MyParent4这种形式。其父类是Object。

(这是为什么呢?书上这样写的 emm)


5 类和接口初始化的区别

这个暂时没有比较好的代码例子,只好直接上结论了。

结论: 当一个接口在初始化时,并不要求其父接口完成初始化;只有真正使用到父接口时(如引用接口中所定义的常量时),才会初始化。

在这里插入图片描述

//2020-1-31 更新
看例子,

package com.cj.jvm.classloader;

// 接口初始化相关

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyParent5.str);
    }
}

interface MyGranParent5{
    Thread t = new Thread(){
        {
            System.out.println("MyGranParent is here ");
        }
    };
}

class MyParent5 implements MyGranParent5{
    static {
        System.out.println("MyParent5 is here");
    }
    public static  String str = "gggg";
}

结果:
在这里插入图片描述
分析:初始化一个类时,不会是初始化其实现的接口,

修改下代码,打印t变量

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyParent5.t);
    }
}

结果:
在这里插入图片描述
只初始化了MyGranparent接口。

验证,子接口初始化,不会引起父接口的初始化,

package com.cj.jvm.classloader;

// 接口初始化相关

import java.util.UUID;

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.id);
    }
}

interface MyGranParent5{
    Thread t = new Thread(){
        {
            System.out.println("MyGranParent is here ");
        }
    };
}


interface MyChild5 extends MyGranParent5{
    Thread t1 = new Thread(){
        {
            System.out.println("MyChild5 is here ");
        }
    };
    public static final String id = UUID.randomUUID().toString();
}

结果:
在这里插入图片描述

分析:
MyChild5的父接口MyGranparent5并没有初始化

6 初始化的顺序

看代码:

package com.cj.jvm.classloader;

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();

        System.out.println("count1 = "+ Singleton.count1);
        System.out.println("count2 = "+ Singleton.count2);
    }
}


class Singleton{
    public static int count1;
    public static int count2=0;
    private static Singleton singleton = new Singleton();
    private Singleton(){
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

结果是:
在这里插入图片描述

分析:显然,得出结果,count1和count2在连接阶段的准备过程中被初始化为默认值0。在构造函数中都增加1,所以结果都为1。

我们修改下几个静态变量的和构造函数的顺序,

package com.cj.jvm.classloader;

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();

        System.out.println("count1 = "+ Singleton.count1);
        System.out.println("count2 = "+ Singleton.count2);
    }
}


class Singleton{
    public static int count1;
    private static Singleton singleton = new Singleton();
    private Singleton() {
        count1++;
        count2++;
    }
    public static int count2=0;
    public static Singleton getInstance(){
        return singleton;
    }
}

结果:
在这里插入图片描述

分析:
调整顺序之后,结果发生了改变,说明,程序执行的顺序应该是:

准备阶段,默认初始化为0–》构造函数对变量进行修改–》类初始化,给变量赋予正确的值。

再修改下程序,

package com.cj.jvm.classloader;

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();

        System.out.println("count1 = "+ Singleton.count1);
        System.out.println("count2 = "+ Singleton.count2);
    }
}


class Singleton{

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

        System.out.println(count1);
        System.out.println(count1);

        count1++;
        count2++;

        System.out.println(count1);
        System.out.println(count1);
    }
    public static int count2=0;
    public static int count1=2;

    public static Singleton getInstance(){
        return singleton;
    }
}

结果:
在这里插入图片描述

7 反射导致的类初始化

看例子(20-1-31更新)

package com.cj.jvm.classloader;

class CJ{
    static {
        System.out.println("class CJ static block");
    }
}
public class MyTest8 {
    public static void main(String[] args) throws ClassNotFoundException {
        //获取系统类加载器
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //使用系统类加载器去加载CJ类
        Class<?> clazz = classLoader.loadClass("com.cj.jvm.classloader.CJ");
        System.out.println(clazz);

        System.out.println("--------------");
        Class<?> clazz2 = Class.forName("com.cj.jvm.classloader.CJ");
        System.out.println(clazz2);

    }
}

结果:
在这里插入图片描述
分析:系统类加载器加载CJ类,不会导致CJ的初始化。反射会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值