JVM类加载机制分析小记

学海无涯,旅“途”漫漫,“途”中小记,如有错误,敬请指出,在此拜谢!

一、前情提要

今天看到了一个关于JVM类加载的题,绞尽脑汁未做对,研究研究,题目如下

package com.example.demo;

public class Test {
    public static int k = 0;
    public static Test t1 = new Test("t1");
    public static Test t2 = new Test("t2");
    public static int i = print("i");
    public static int n = 99;
    private int a = 0;
    public int j = print("j");

    {
        print("构造块");
    }

    static {
        print("静态块");
    }

    public Test(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++i;
        ++n;
    }

    public static int print(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String args[]) {
        Test t = new Test("init");
    }
}

那么见证奇迹的时刻来了,输出结果为:

1:j    i=0     n=0
2:构造块    i=1     n=1
3:t1    i=2     n=2
4:j    i=3     n=3
5:构造块    i=4     n=4
6:t2    i=5     n=5
7:i    i=6     n=6
8:静态块    i=7     n=99
9:j    i=8     n=100
10:构造块    i=9     n=101
11:init    i=10     n=102

不知道有多少小伙伴做对了呢?反正我只是做对了20%,不及格。

二、理论基础

1、类加载生命周期

首先,我们需要铭记下面这个图片,也就是jvm加载类的生命周期。所以一个类在被使用之前,经历了加载->验证->准备->解析->初始化五个过程(有的书中也会把验证+准备+解析统一叫做连接)。
在这里插入图片描述
那么,每个过程,jvm都对类做了哪些不可告人的事情呢?

1.1加载

在java程序运行之前,JVM会对类进行加载。在此过程中,JVM会把编译完成的.class二进制文件加载到内存,后续提供程序使用,用到的就是类加载器ClassLoader 。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段就可能开始了。但是夹在加载阶段进行的动作,仍然属于连接阶段的内容。

1.2连接-验证

验证是连接的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全。 验证阶段的四个步骤:文件格式检验、元数据检验、字节码检验、符号引用检验。

文件格式检验:检验字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
元数据检验:对字节码描述的信息进行语义分析,以保证其描述的内容符合Java语言规范的要求。
字节码检验:通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。
符号引用检验:符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

1.3连接-准备

该阶段正式为类变量分配内存并设置类变量初始值。这些变量所使用的内存将在方法区中进行分配。此时进行内存分配的仅包括类变量,而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。另外,在这里分配的静态类变量是将其值定义为默认值。因为在该阶段并未执行任何Java方法,正确的赋值将在初始化阶段执行。

1.4连接-解析

该阶段虚拟机会将常量池内的符号引用替换为直接引用的过程。

1.5初始化

这是类加载的最后一步,真正执行类中定义的字节码,也就是.class文件。 初始化阶段是执行类构造器方法的过程,以及真正初始化类变量和其他资源的过程。

2、名词含义以及区别

2.1 构造代码块、构造函数代码块、静态代码块区别

(1)构造代码块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块。构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。比如:

{
    print("构造块");
}

(2)构造函数:用于给对象进行初始化,是给与之对应的对象进行初始化,它具有针对性,函数中的一种。比如

public Test(String str) {...}

(3)静态代码块:static{}包裹的代码块,且静态代码只执行一次,可以通过Class.forName(“classPath”)的方式唤醒代码的static代码块,但是也执行一次。只执行一次的原因,百度了一下,大概是类被加载进内存中的方法区的时候调用静态代码块,而加载类到内存中只需要执行一次即可,比如

static{
	System.out.println("static代码块");
}

(4)特点:
1:该函数的名称和所在类的名称相同。
2:不需要定义返回值类型。
3:该函数没有具体的返回值。
(5)底层分析:通过反编译可以看到,构造代码块中的代码也是在构造方法中执行的。在编译时的编译器看来会默认将构造代码块中的代码移动到构造方法中,并且移动到构造方法内容的前面。
(6)三者的顺序
显示static代码初始化,然后是构造方法初始化,然后是构造函数初始化,并且静态代码只会初始化一次。比如测试方法

public class Test2 {
    public Test2() {
        System.out.println("HaHa:我是构造函数代码块");
    }

    {
        System.out.println("HeHe:我是构造代码块");
    }

    static {
        System.out.println("HoHo:我是静态代码块");
    }

    public static void main(String[] args) {
        new Test2();
        new Test2();
    }
}

得到的结果为

HoHo:我是静态代码块
HeHe:我是构造代码块
HaHa:我是构造函数代码块
HeHe:我是构造代码块
HaHa:我是构造函数代码块

(7)加上继承
假设Test3继承上面的Test2,代码如下

public class Test3 extends Test2 {
    public Test3() {
        System.out.println("Test3:HaHa:我是构造函数代码块");
    }

    {
        System.out.println("Test3:HeHe:我是构造代码块");
    }

    static {
        System.out.println("Test:3HoHo:我是静态代码块");
    }

    public static void main(String[] args) {
        new Test3();
        new Test3();
    }
}

输出结果为

HoHo:我是静态代码块
Test:3HoHo:我是静态代码块
HeHe:我是构造代码块
HaHa:我是构造函数代码块
Test3:HeHe:我是构造代码块
Test3:HaHa:我是构造函数代码块
HeHe:我是构造代码块
HaHa:我是构造函数代码块
Test3:HeHe:我是构造代码块
Test3:HaHa:我是构造函数代码块

可以看出父类相同的类型,会在子类之前执行

三、问题分析

当方法执行时,先加载Test类,加载Test类时,会按顺序加载静态域(见四的解释A),所以先执行

public static int k = 0;//1

然后执行

public static Test t1 = new Test("t1");//2

当执行该方法时,创建Test对象,就会运行Test类中的构造方法,即

public int j = print("j");//2.1

{
    print("构造块");//2.2
}

public Test(String str) {...}//2.3(str为t1)

然后执行

public static Test t1 = new Test("t2");//3

当执行该方法时,创建Test对象,就会运行Test类中的构造方法,即

public int j = print("j");//3.1

{
    print("构造块");//3.2
}

public Test(String str) {...}//3.3(str为t2)

然后执行

public static int i = print("i");//4

然后执行

public static int n = 99;//5

然后执行

static {
    print("静态块");//6
}

执行到此处,Test类的静态域加载完毕,然后开始执行main函数中的代码

public static void main(String args[]) {
    Test t = new Test("init");//7
}

当执行该方法时,创建Test对象,就会运行Test类中的构造方法,即

public int j = print("j");//7.1

{
    print("构造块");//7.2
}

public Test(String str) {...}//7.3(str为init)

四、解释

解释A

如果在main函数中增加输出如下

 public static void main(String args[]) {
     System.out.println("haha");//增加此输出
     Test t = new Test("init");
 }

则运行后的输出为

1:j    i=0     n=0
2:构造块    i=1     n=1
3:t1    i=2     n=2
4:j    i=3     n=3
5:构造块    i=4     n=4
6:t2    i=5     n=5
7:i    i=6     n=6
8:静态块    i=7     n=99
haha
9:j    i=8     n=100
10:构造块    i=9     n=101
11:init    i=10     n=102

则可以看出,步骤1-8为加载类的时候输出的,9-11为main函数输出。

五、其他

参考文献:
https://my.oschina.net/u/1458864/blog/2004785
https://baijiahao.baidu.com/s?id=1633972974070851508&wfr=spider&for=pc
https://yq.aliyun.com/articles/712207
http://www.sohu.com/a/225428891_819383
https://blog.csdn.net/hxhaaj/article/details/81174743
https://www.cnblogs.com/Heliner/p/10524699.html

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Doubletree_lin

老板,爱你,么么哒

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值