JVM相关流程的总结

JVM相关流程理解

记录和思考,不断完善中...

类加载的流程

java的类加载机制是按需加载,当需要某个类时,通过类的全限定名或其他方式获取对应的字节码文件,然后类加载器会采用双亲委派模型对字节码文件进行加载,即首先尝试使用父类加载器进行加载,如果加载不了,再交由子类加载器进行加载,这种加载方式可以有效保护java的核心类库。加载的过程如下:

加载阶段

就是将类的字节码加载到内存中并生成代表该类的Class对象,这个阶段中类的静态存储结构转化为方法区的运行时数据结构,而创建的Class对象就是方法区中这个类的数据访问入口

个人理解:

可以认为类加载的过程中,在加载阶段就完成了外部类到JVM内的加载过程,因为此时确实已经在堆区创建了代表类的class对象了。加载过程的后续环节可以认为是对class对象的不断丰富,比如校验,初始化等。

链接

包含验证、准备、解析三个小阶段

验证

验证字节码合法性

思考:为什么在加载之前不进行验证呢?如果字节码不合法,岂不是进行一次无用的加载。

Class对象是类在JVM中的表现形式,需要在加载进JVM并生成相应的代表此类的Class对象后,才能通过Class对象对该类的元数据进行各种判断验证,以确保该class不会对JVM运行产生危害。

准备

对类对象的静态变量设置默认初始值。基本类型,如int设置为0,对象则设置为null。

解析

这个过程则是将类对象中的符号引用解析成直接引用。这一阶段的主要任务是解析类的常量池中的符号引用

扩展:

  • 符号引用: 在 Java 源代码中,我们使用的类名、方法名、字段名等都是符号引用。这些符号引用在编译时是无法直接定位到内存地址的,而是需要在运行时动态解析。
  • 直接引用: 直接引用是可以直接定位到内存地址的引用,是符号引用经过解析后的结果。直接引用使得虚拟机能够快速访问到对应的类、方法或字段的内存地址,而不需要再进行一次动态的解析过程。

初始化

这个阶段是为了让类对象完成初始化并处于可用状态。

当类中包含静态变量或静态代码块时,JVM会生成一个clinit方法用来包含这些静态代码并执行,如果没有静态代码,则不会生成clinit方法。

注意:

与链接阶段中对静态变量分配内存并设置默认初始值不同,此阶段是显示进行赋值,区别如下:

java 复制代码
public class Example {
    // 静态变量在链接阶段的准备阶段分配内存并设置零初始值
    // 静态变量 num 被初始化为 0
    // 静态变量 str 被初始化为 null
    // 注意:这只是准备阶段的设置,默认初始值,并没有执行具体的赋值操作
    public static int num;
    public static String str;

    static {
        // 静态代码块中的赋值操作,将 num 和 str 初始化为实际的初始值
        num = 42;
        str = "Hello, World!";
    }

    public static void main(String[] args) {
        // 在初始化阶段,静态变量 num 和 str 被赋予实际的初始值
        System.out.println(num); // 输出 42
        System.out.println(str); // 输出 Hello, World!
    }
}

对象的创建流程

  1. 检查类是否已加载
  2. 在堆中为对象的创建分配内存
  3. 初始化内存(基本数据类初始化)
  4. 设置对象头(mark word和类指针)
  5. 执行构造方法,返回新创建对象的引用 (Class对象的创建略有不同,是通过JVM直接设置相关属性完成最终初始化的)

思考:

  1. 调用构造函数之前,对象还没有创建实例,没有对象实例该如何设置对象头?
    在内存分配和对象头设置完成后,JVM会持有这个内存地址的引用。这个引用相当于"对象实例",但还没有完全初始化,JVM会将这个引用传递给构造函数。在构造函数中,this引用指向的就是这个已经分配了内存和设置了对象头的内存区域。
  2. 对象创建的过程是看不见的,是否可以通过代码或工具证明创建过程
    // 字节码参考java虚拟机规范
java 复制代码
public class Test {
    public static void main(String[] args) {
        Test obj = new Test();
    }
}
//javap -c Test   反编译结果

0: new           #2                  // 创建对象 ,未完整创建 
3: dup                               // 复制对象引用
4: invokespecial #1                  // 调用构造函数
7: astore_1                          // 保存引用
8: return                            // 返回

// 说明:
//new 指令不会完全创建新实例;直到对未初始化的实例调用 
//实例初始化方法invokespecial指令

从字节码可以看到,JVM先创建对象(堆区分配了内存且设置了对象头的内存区域)并复制引用,然后调用构造函数,构造函数中this将引用内存区域的地址。

思考:

对象实例只是JVM中存储了数据的一片内存空间,实例化、初始化都是通过在内存中存储数据所呈现的不同的状态或阶段。

如何定位对象

JVM中有两种定位对象的方式,不同的JVM实现,定位方式可能不同。

  • 通过句柄定位对象
  • 通过直接指针定位对象

句柄

shell 复制代码
+------------+     +------------+      +------------------+
|   引用      | --> |   句柄表    | --> |    对象实例       |
+------------+     +------------+      +------------------+
| ref: handle |     | handle: obj_ptr |      |  data      |
+------------+     +------------+      +------------------+

在句柄方式中,引用变量包含一个指向句柄表的指针。句柄表中的每个条目包含指向实际对象实例数据的指针。
优点 :当对象在内存中移动时,只需要更新句柄表中的指针即可,而不需要修改引用本身。(个人感觉有点像代码中的防腐层)
缺点:复杂,需要额外维护句柄表

直接指针

shell 复制代码
+------------+      +------------------+
|   引用      |  --> |    对象实例       |
+------------+      +------------------+
| ref: obj_ptr |      |     data       |
+------------+      +------------------+

在直接指针方式中,引用变量直接指向对象在堆中的实际内存地址。这样,当引用访问对象时,只需一次内存定位操作即可。
优点 :直接指针访问更快,因为少了一次间接访问。
缺点:对象在堆中的移动和内存压缩(Compact),需要修改所有引用。

相关推荐
大数据编程之光22 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
爪哇学长36 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
ExiFengs40 分钟前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj12345678941 分钟前
JDK1.8新增特性
java·开发语言
繁依Fanyi1 小时前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:打开 PDF 时自动显示书签或缩略图
java·pdf·.net
m51271 小时前
LinuxC语言
java·服务器·前端
IU宝1 小时前
C/C++内存管理
java·c语言·c++
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
hakesashou1 小时前
Python中常用的函数介绍
java·网络·python