Featured image of post 深入 JVM(一)

深入 JVM(一)

深入理解 jvm 相关知识,本篇讲解类的初始化过程已经其中一些容易让我们忽略的东西

闲来无事,想着拾掇拾掇自己丢弃的知识,一来为了更加深入了解一下jvm相关原理,二来为自己后续开发和学习java agentasm相关知识打个基础;本篇主要通过代码来驱动记忆和学习之前一直没有注意的java类加载的过程,话不多说,学习起来!

类加载

类加载过程

类的加载总的过程主要分为加载 -> 连接 -> 初始化 -> 使用 -> 卸载,其中连接可以细化为验证 -> 准备 -> 解析,其大概过程如下图所示:

类加载过程

由于使用卸载不是十分重要,所以我们这里主要针对前面几个状态进行讲解。

加载

加载过程中,首先会通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。注意,java虚拟机规范中并没有说明Class对象位于哪里,所以依据虚拟机的不同,保存java对象的位置也是不同,例如HotSpot虚拟机就是将这些对象放置在方法区中。

连接

  • 验证:确保被加载的类的正确性;
  • 准备:为类的静态变量分配内存,并将其初始化为默认值
  • 解析:把类中的符号引用转换为直接引用,即可以使用指针的方式直接定位到目标对象、方法或者成员变量的位置

初始化

将静态变量真正的值赋予到静态字段中,替换在程序准备阶段为静态变量赋予的默认值

类的使用方式

所有的java虚拟机实现必须在每个类或者接口被java程序首次使用时才初始化他们。

主动使用

  • 创建类的实例
  • 访问某个类或者接口的静态变量,或者为静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类,父类也会初始化
  • java虚拟机启动时,这个被被标记为启动类的类
  • jdk1.7开始的提供动态语言支持

被动使用

除了上述讲的主动使用外的类的使用,都被看作类的被动使用,都不会导致类的初始化

示例

【示例一】父子类的初始化

片段一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/9/22
 */
public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str);
    }
}

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
2
myparent1 static block
hello world

片段二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/9/22
 */
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
2
3
myparent1 static block
mychild1 static block
welcome

以上两个片段中,我们仅仅改变了输出的字段,由父类的字段str修改成了子类的str2字段,但是输出结果却发生了较为重大的变化,从输出结果中我们可以看到,片段一中仅仅初始化了MyParent1并没有初始化子类,而片段二中却初始化了父类和子类,根据我们上面提到的类的初始化的主动使用方式,我们可以看到,片段一仅仅是调用了父类的字段,并没有使用到子类的相关字段,所以子类并没有进行初始化,而片段二中,代码调用了子类的字段,依据主动使用相关定义,由于初始化子类,需要先初始化父类,所以这里会先输出父类的静态代码块。

【示例二】类的初始化与常量

片段一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.yinan.classloader;

import java.util.UUID;

/**
 * @author yinan
 * @date 2019/9/23
 */
public class Mytest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

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

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

输出结果

1
2
myparent2 static block
hello world

片段二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.yinan.classloader;

import java.util.UUID;

/**
 * @author yinan
 * @date 2019/9/23
 */
public class Mytest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

class MyParent2 {
    public static final String str = "hello world";

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

输出结果

1
hello world

片段三

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.yinan.classloader;

import java.util.UUID;

/**
 * @author yinan
 * @date 2019/9/23
 */
public class Mytest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

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

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

输出结果

1
2
myparent2 static block
0251b6ff-7eaf-498e-b5a0-ef782004bf78

综合上述三个片段,我们可以看到,针对调用在程序编译期间可以确定值的常量,定义这个常量的类并不会进行初始化,而如果调用某个类的普通的静态变量或者在程序编译阶段无法确定值的常量,是会导致这个类的初始化工作。常量在编译阶段会存入到调用该常量的方法所在类的常量池中,本质上,调用类并没有直接引用定义常量的类,因此并不会触发定义常量的类的初始化。在片段二中,这个常量str在编译阶段会被放入Mytest2这个类的常量池中,所以在片段二程序编译之后,我们如果删除已经编译好的MyParent2.class文件,也是可以继续运行的。

【示例三】类初始化与数组

片段一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/9/23
 */
public class MyTest4 {
    public static void main(String[] args) {
       MyParent4 parent4 = new MyParent4();
    }
}

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

输出结果

1
MyParent4 static block

片段二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/9/23
 */
public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] parent4s = new MyParent4[1];
    }
}

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

输出结果(为空)

从上面两个片段,我们可以看到针对数组类型的初始化,并不会导致数组定义的类初始化,进而我们可以猜想,数组是一种新的类型,而不是原始的类型,我们可以输出这个对象对应的类,通过System.out.println(parent4s.getClass());可以查看当前对象所对应的类型,我们可以看到数组对象对应的类型是class [Lcn.yinan.classloader.MyParent4;而不是原来的class cn.yinan.classloader.MyParent4类型,这个新的类型是java代码运行时生成的新的类型。

【示例四】类的初始化顺序

片段一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/10/4
 */
public class MyTest7 {

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1: " + Singleton.counter1);
        System.out.println("counter2: " + Singleton.counter2);
    }

    static class Singleton {
        public static int counter1;

        public static int counter2 = 0;

        private static Singleton singleton = new Singleton();

        private Singleton() {
            counter1 ++;
            counter2 ++;
        }

        public static Singleton getInstance() {
            return singleton;
        }



    }
}

输出结果

1
2
counter1: 1
counter2: 1

上面的代码片段我们很容易理解,程序主动使用Singleton类, 触发Singleton的初始化,由于在程序的准备阶段会为counter1counter2赋予默认值0,所以在程序显示初始化时,会先为counter1counter2进行显示初始化,由于counter1没有赋予值,所以为0,而counter2因为显示地进行了初始化,因此会被赋予显示的值0,而最后进行构造方法的初始化时,在程序初始化结束,counter1counter2的值都变成了1

片段二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.yinan.classloader;

/**
 * @author yinan
 * @date 2019/10/4
 */
public class MyTest7 {

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1: " + Singleton.counter1);
        System.out.println("counter2: " + Singleton.counter2);
    }

    static class Singleton {
        public static int counter1;
        
        private static Singleton singleton = new Singleton();

        private Singleton() {
            counter1 ++;
            counter2 ++;
        }

        public static int counter2 = 0;

        public static Singleton getInstance() {
            return singleton;
        }



    }
}

输出结果

1
2
counter1: 1
counter2: 0

通过对片段一的分析,我们就很好地理解了片段二的结果了,在程序显示初始化构造方法阶段,counter1counter2的值都是变成了1,然后执行counter2的初始化,此时counter2的值变成了0,因此输出的值就变成了counter1: 1以及counter2: 0

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus