JVM相关流程的总结

avatar
作者
猴君
阅读量:0

JVM相关流程理解

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

类加载的流程

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

加载阶段

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

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

链接

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

验证

验证字节码合法性

思考:为什么在加载之前不进行验证呢?如果字节码不合法,岂不是进行一次无用的加载。
Class对象是类在JVM中的表现形式,需要在加载进JVM并生成相应的代表此类的Class对象后,才能通过Class对象对该类的元数据进行各种判断验证,以确保该class不会对JVM运行产生危害。

准备

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

解析

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

扩展:

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

初始化

这个阶段是为了让类对象完成初始化并处于可用状态。
当类中包含静态变量或静态代码块时,JVM会生成一个clinit方法用来包含这些静态代码并执行,如果没有静态代码,则不会生成clinit方法。

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

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虚拟机规范
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实现,定位方式可能不同。

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

句柄

+------------+     +------------+      +------------------+ |   引用      | --> |   句柄表    | --> |    对象实例       | +------------+     +------------+      +------------------+ | ref: handle |     | handle: obj_ptr |      |  data      | +------------+     +------------+      +------------------+ 

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

直接指针

+------------+      +------------------+ |   引用      |  --> |    对象实例       | +------------+      +------------------+ | ref: obj_ptr |      |     data       | +------------+      +------------------+ 

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

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!