用code带你了解jvm的类加载过程

这几天学习jvm一些知识,刚好在牛客网上刷题时遇到一道关于jvm底层编译顺序的题目,看到问题也都是就java代码而言讲的,也没有剖析原理。我趁着所学jvm还热着,赶紧分享一下我的拙见,不然过不了多久估计我也不会了。

引子

在牛客看到题目是这样的:

public class Test {
    static int x=10;
    static {x+=5;}
    public static void main(String[] args) 
        {
        System.out.println("x="+x);
    }
    static{x/=3;};
}

上面代码能否运行,如果能够运行,那最后输出的结果是什么?

这里你先思考一下,再继续阅读
ps:防止大家不信我特意用idea编译运行一遍,把截图奉上以示自己正确性。

这一串代码是可以运行的,结果为 5,和你思考的一样吗?其实这不单纯是 x=10; 然后x+=5;x值变成15,在最后x/=3;x的值变成5那么简单的哦。
Test_0
这里我先不分析原因,直接从jvm加载开始讲解,等到讲完大家也就知道原因了。

准备阶段

讲类加载过程其实还需要了解class类怎么在jvm中的存储的。
如果说class是java的类(java代码)
那么InstanceKlass就是jave类在jvm中的存在形式(c++代码)

classloader(类加载过程)

首先大家要清楚一点就是jvm加载是懒加载(lazy loading)。懒加载通俗讲就是偷懒,用到谁加载谁,用不到就不加载。

类的生命周期是由7个阶段组成,但是类的加载说的是前5个阶段
类的生命周期
大致讲解5个阶段功能:

  1. 加载:就是加载class文件到jvm中
  2. 验证:验证文件格式,元数据,字节码等
  3. 准备:为静态变量分配内存,赋初值(ps:实例对象new时直接赋值在准备阶段没有赋初值一说。如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步)
  4. 解析:将常量池中的符号引用转为直接引用
  5. 初始化:执行静态代码块,完成静态变量的赋值。静态字段、静态代码段,字节码层面会生成clinit方法。clinit方法中语句的先后顺序与代码的编写顺序相关(仔细看这一句)

这里补充一点:加载阶段主动使用子类父类也需要加载

我们看一下上面题目的字节码:
在这里插入图片描述
因为这一题,main方法肯定使用时在类加载过程的后面,所以到初始化的时候就会按顺序把static int x = 10; static {x+=5;} static{x/=3}执行。但其实这个题目比较简单,我们看几个复杂的例子,让大家彻底搞懂这块知识。

Talk is cheap,show me your code.

正如标题所说,我们直接来看代码

Test_1

大家可以先看代码,然后自己去思考输出,然后再看我的解析会好点。

public class Test_1 {
    public static void main(String[] args) {
        System.out.printf(Test_1_B.str);

        while (true);
    }
}

class Test_1_A {
    public static String str = "A str";

    static {
        System.out.println("A Static Block");
    }
}

class Test_1_B extends Test_1_A {
    static {
        System.out.println("B Static Block");
    }
}

结果是:
A Static Block
A str
Test_1结果
差异吗?是不是很惊奇,待我慢慢讲来。

  1. 我们要明白jvm中类加载是懒加载模型,所以用不到就不加载
  2. static String str = “A str”;其实它是存储在Test_1_A 的InstanceMirrorKlass中的
    是否明白?而Test_1_B中是没有str的,所以main中调用时调用的其实是A的str,B用不到自然就不用加载了。

附加 证明static String str = “A str”;其实它是存储在Test_1_A 的InstanceMirrorKlass中的。这个证明我们需要用到HSDB(不懂得百度一个神器)
使用步骤就是:
先jps-l查看进程号
使用hsdb通过进程号查看进程的内存地址
通过内存地址搜索到存储的内容如下图(Test_1_A的jvm存储的信息,看到str被存在A中):

在这里插入图片描述
这里B就不演示搜索了,其实B中InstanceMirrorKlass是什么都没有的,也就能证明str这个静态String被存到A中。

Test_2

public class Test_2 {

    public static void main(String[] args) {
        System.out.printf(Test_2_B.str);
    }
}

class Test_2_A {
    static {
        System.out.println("A Static Block");
    }
}

class Test_2_B extends Test_2_A {
    public static String str = "B str";

    static {
        System.out.println("B Static Block");
    }
}

结果是:
A Static Block
B Static Block
B str
在这里插入图片描述
踩了第一个坑之后,第二个就好点了吧。这个如果做错就是上面有一句话没有注意:加载阶段主动使用子类父类也需要加载,那就是你调用子类,父类会间接调用。第一题告诉我们调用父类,却不会调用子类。

Test_3

仔细审视,这和第二题是有区别的。区别在于final。

public class Test_3{

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

class Test_3_A {
    public static final String str = "A Str";

    static {
        System.out.println("Test_6_A Static Block");
    }
}

结果是:A Str
在这里插入图片描述
这里牵扯到我前面介绍的另外一个点:

准备:为静态变量分配内存,赋初值(ps:实例对象new时直接赋值在准备阶段没有赋初值一说。如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步)

上面也有这句话,你理解其中含义没?其实final修饰的,在编译过程中将str=A Str写入Test_6_A的常量池中,所以不需要调用Test_6_A,直接在常量池中就能找到(真是有够懒加载的对不···)
可以使用javap -vervose classpath 查询常量池
在这里插入图片描述
三个例子够不够?不够那再来,码字不易记得多多点赞分享

Test_4

public class Test_4 {

    public static void main(String[] args) {
        System.out.println(Test_4_A.uuid);
    }
}

class Test_4_A {
    public static final String uuid = UUID.randomUUID().toString();

    static {
        System.out.println("Test_4_A Static Block");
    }
}

结果:
Test_4_A Static Block
38b41380-7111-4ce5-8079-5a2576fd4282

这个uuid虽说被finla修饰,但是右边是需要动态创建才能生成的,所以结果才会这样。

Test_5

public class Test_5 {

    public static void main(String[] args) {
        Test_5_A obj = Test_5_A.getInstance();

        System.out.println(Test_5_A.val1);
        System.out.println(Test_5_A.val2);
    }
}

class Test_5_A {

    public static int val1;
    public static int val2 = 1;

    public static Test_5_A instance = new Test_5_A();

     Test_5_A() {
        val1++;
        val2++;
    }

    public static Test_5_A getInstance() {
        return instance;
    }
}

结果是:1 2
这里我们一步一步分析,在前面的初始化阶段中:clinit方法中语句的先后顺序与代码的编写顺序相关,故val1=0;val2=1;
public static Test_5_A instance = new Test_5_A();这个会调用构造函数Test_5_A(),导致val1++,和val2++;
所以最后结果是1和2

Test_6

public class Test_6 {

    public static void main(String[] args) {
        Test_6_A obj = Test_6_A.getInstance();
        System.out.println(Test_6_A.val1);
        System.out.println(Test_6_A.val2);
    }
}

class Test_6_A {
    public static int val1;
    public static Test_6_A instance = new Test_6_A();
     Test_6_A() {
        val1++;
        val2++;
    }
    public static int val2 = 1;
    public static Test_6_A getInstance() {
        return instance;
    }
}

结果是:1 1
还是clinit方法中语句的先后顺序与代码的编写顺序相关,这次初始化顺序是val1=0;然后初始化public static Test_6_A instance = new Test_6_A();调用Test_6_A(),val1++,val2++;最后再次初始化val2=1;所以这里因为顺序关系导致覆盖。

感谢大家的观看,有什么错误之处欢迎大家指出来。

猜你喜欢

转载自blog.csdn.net/qq_40994080/article/details/107930254