JVM-2

JVM-2

jvm的组成

1)类加载器:ClassLoader

2)运行时数据区域(JVM管理的内存)

3)执行引擎(及时编译器,解释器,垃圾回收器等)

4)本地接口(C++编写的底层调用接口)

类的生命周期-☆

生命周期描述了一个类从被加载到使用,卸载的过程

类的生命周期-应用场景

  1. 类加载器的作用
  2. 类的加密和解密
  3. 多态的原理
  4. 运行时常量池

学习目录

  1. 生命周期的概述

  2. 加载阶段

  3. 链接阶段

  4. 初始化阶段

1)概述

五个阶段:加载-》连接-》初始化-》使用-》卸载(垃圾回收)

有些教材会出现七个阶段:这是因为 2连接 做的事情比较多,会被拆分为 验证,准备,解析 三个

重点阶段:初始化阶段

2)加载阶段

加载(Loading)阶段第一步是类加载器,根据类的全限定名通过不同的渠道以二进制流的方式回去字节码信息。

-程序员可以使用java代码扩展的不同的渠道:

-本地字节码文件,动态代理生成,通过网络传输的类等。

  1. 类加载器在加载完类之后,java虚拟机会将字节码中的信息保存到方法区中。

  2. 生成一个InstanceKlass对象,保存类的所有信息,里面还包含时间特定功能比如多态的信息。

    1. image-20240921110828445
  3. 同时,Java虚拟机还会在 堆 中生成一份与方法区中数据类似的java.lang.Class对象(作用是在Java代码中区获取类的信息以及存储静态字段的数据(JDK8及以后))

    image-20240921111241415

    image-20240921111115631

Q:为什么需要再 堆区 复制一份 方法区 的字节码文件呢?这样不浪费内存吗?

A:方法区的instanceKlass是使用C编写的,开发者无法使用Java区访问,而堆区的Java.lang.Class是java编写的,开发者可以访问。并且,方法区里面的东西不是全部都会使用,把会用到的放在堆区使用就行。这样Java虚拟机就能更好的控制开发者访问数据的权限。

image-20240921111651159

类的加载阶段 - 查看内存中的对象

  • 推荐使用JDK自带的hsdb工具查看java虚拟机中的内存信息。工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中。

  • java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
    
    // 为什么不使用java -jar呢
    // 因为这个jar包里面有很多启动类,需要指定启动那一个启动类。
    
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97



    3)连接阶段

    1. 验证:校验字节码是否满足《JAVA虚拟机规范》

    Linking阶段,程序员不需要参与

    验证的步骤是非常复杂的,主要包含以下四个步骤:

    1:文件格式校验

    字节码文件是否以CAFEBABE开头,主次版本号是否满足当前Java虚拟机

    2:元信息校验:例如类,必须有父类

    3:校验字节码指令是否准确

    4:校验符号引用,例如是否访问其他类中的private的方法。



    2. 准备:给静态变量赋初始值

    ![image-20240921122941591](JVM-2/image-20240921122941591.png)

    ![image-20240921122955900](JVM-2/image-20240921122955900.png)

    如果是final修饰的,直接赋值(因为是不会发生变化的,直接赋值)

    ![image-20240921123101385](JVM-2/image-20240921123101385.png)

    3. 解析:将常量池中的符号引用缓存指向内存的直接引用

    ![image-20240921123423013](JVM-2/image-20240921123423013.png)



    4)初始化阶段(与程序员有关)

    前面说的,如果变量不是final修饰的,都是赋初始值,不是指定的常量值,而在初始化阶段,会执行静态代码块的代码,并为静态变量赋值。

    初始化阶段会执行字节码文件中 clinit 部分的字节码指令

    ![image-20240921130459507](JVM-2/image-20240921130459507.png)

    这里是先定义value,赋值为0,执行static赋值为2,在执行赋值为1,最后结果就为1





    以下几种方式会导致类的初始化:

    1. 访问一个类的静态变量或静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。

    2. 调用Class.forName(String className)。

    forName方法有一个需要注意的,他有一个重载,如果只传入className,他会调用另一个方法forName0(),会传入一个boolean initalize参数表示需要初始化,也可以在调用forName时,指定需不需要初始化forName(className,initialize,classLoader)

    3. new一个类的对象时
    4. 执行Main方法的当前类



    ```java
    // 例题
    public class Test1 {
    public static void main(String[] args) {
    System.out.println("A");
    new Test1();
    new Test1();
    }

    public Test1() {
    System.out.println("B");
    }

    {
    System.out.println("C");
    }

    static {
    System.out.println("D");
    }

    /***
    * 静态代码块在类加载的时候只执行一次。程序一开始,就会先打印出 "D"。
    * 程序的入口Main,就会先打印出 "A"。
    * 创建 Test1 对象 (new Test1();):
    * 初始化实例代码块 ({}): 实例代码块在每个对象创建时都会执行,所以会打印出 "C"。
    * 构造方法 (public Test1()): 构造方法用于初始化对象,在这里会打印出 "B"。
    * <p>
    * DACBCB
    */
    }

以下几种方式不会导致类的初始化:

  1. 无静态代码块且无金泰变量赋值语句。

  2. 有静态变量的声明,但没有赋值语句

    1
    public static int a;
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。

    1
    public final static int a = 10;
  • 直接访问父类的静态变量,不会触发子类的初始化
  • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。
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
public class Test2 {
public static void main(String[] args) {
new B02();
System.out.println(B02.a);
}
}

class A02{
static int a = 0;
static {
a = 1;
}
}

class B02 extends A02{
static {
a = 2;
}
}

/***
* 解释:
* 类加载顺序:
* 由于 B02 继承了 A02,并且 main 方法中创建了 B02 对象,所以 A02 会先加载。
* 在加载 A02 时,会执行其静态代码块,将 a 初始化为 1。
* 接着,B02 会加载,执行其静态代码块,将 a 重新赋值为 2。
* 静态变量的特性:
* 静态变量属于类,而不是对象。
* 因此,a 在 A02 和 B02 中都是同一个变量。
* 程序执行流程:
* new B02();: 创建 B02 对象,触发 B02 的加载,并执行其静态代码块,将 a 赋值为 2。
* System.out.println(B02.a);: 打印 B02 类中的静态变量 a 的值,此时 a 的值为 2。
*/

生命周期总结:

image-20240921134546066

image-20240921134555934


JVM-2
http://example.com/2024/09/21/JVM/JVM-2/
作者
JcenLeung
发布于
2024年9月21日
许可协议