浅析Jvm

浅析Jvm

基本概念

引言

Java 虚拟机(JVM,Java Virtual Machine)是 Java 生态系统的核心组成部分,它为 Java 应用程序提供了一个运行环境。JVM 的主要职责是将 Java 字节码(Bytecode)转换为机器码,并执行这些机器码,从而实现 Java 的“写一次,运行到处”的跨平台特性。

JVM 的基本架构

JVM 的架构可以分为五个主要部分:类加载子系统(Class Loader Subsystem)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地接口(Native Interface)和垃圾回收(Garbage Collection)。

1. 类加载子系统(Class Loader Subsystem)

类加载子系统负责将 Java 类从文件系统或网络中加载到 JVM 中。类加载过程包括以下三个步骤:

  • 加载:找到并加载类的二进制数据。
  • 链接:验证、准备和解析类。
    • 验证:确保类文件的字节码符合 JVM 规范。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值。
    • 解析:将类的符号引用转换为直接引用。
  • 初始化:执行类的静态初始化块和静态变量的赋值操作。

2. 运行时数据区(Runtime Data Area)

运行时数据区是 JVM 在执行 Java 程序时使用的内存区域,它可以进一步细分为以下几个区域:

  • 方法区(Method Area):存储已加载类的元数据、常量池、静态变量和JIT编译后的代码。从 Java 8 开始,方法区被移到本地内存中的元空间(Metaspace)中。
  • 堆(Heap):所有对象实例和数组在这里分配内存。堆是垃圾回收(GC)的主要区域。
  • 栈(Stack):每个线程都有自己的栈,存储局部变量、操作数栈和帧数据。栈中的数据是线程私有的。
  • 程序计数器(Program Counter Register):一个小的内存区域,保存当前线程所执行的字节码指令的地址。
  • 本地方法栈(Native Method Stack):为本地方法(由Native关键字修饰的方法)执行提供栈空间。

3. 执行引擎(Execution Engine)

执行引擎负责执行字节码。它包括以下几个部分:

  • 解释器(Interpreter):将字节码逐条解释执行。解释执行速度较慢,但启动快。
  • 即时编译器(Just-In-Time Compiler, JIT):将热点代码编译为机器码,以提高执行速度。编译后的代码直接在CPU上运行。
  • 垃圾回收器(Garbage Collector):自动管理内存,通过标记和清除、复制和压缩等算法回收不再使用的对象。

4. 本地接口(Native Interface)

本地接口允许 JVM 调用本地库(如 C 或 C++ 编写的库)。通过 JNI(Java Native Interface),Java 程序可以与本地代码进行交互。

垃圾回收(Garbage Collection)

垃圾回收是 JVM 的一个重要特性,用于自动管理内存。JVM 的垃圾回收器会定期扫描堆中的对象,回收不再使用的对象内存。

所谓垃圾回收机制(Garbage Collection, 简称GC),指自动管理动态分配的内存空间的机制,自动回收不再使用的内存,不定时去堆内存中清理不可达对象,以避免内存泄漏和内存溢出的问题。最早是在1960年代提出的。

垃圾回收是 java相较于c、c++语言的优势之一。其他编程语言,如C#、Python和Ruby等,也都提供了垃圾回收机制。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。

这也是垃圾收集器的最主要的缺点。

常见的垃圾回收算法有:

  • 标记-清除算法(Mark-Sweep):标记活动对象,然后清除未标记对象。
  • 标记-复制算法(Mark-Compact):标记活动对象,然后将其复制到新空间,压缩内存。
  • 分代收集算法(Generational GC):将堆分为年轻代(Young Generation)和老年代(Old Generation),分别使用不同的回收算法优化性能。

一次完整的垃圾回收过程是什么样的?

Jvm 垃圾回收的基本过程可以分为以下三个步骤:

  1. 垃圾分类:首先我们的 jvm 在进行垃圾回收的过程,需要确定哪些对象是垃圾对象,哪些对象是存活对象。这个类似于我们在做一件事之前的规划。具体的分类方法一般情况下,垃圾回收器会从堆的根节点(如程序计数器、虚拟机栈、本地方法栈和方法区中的类静态属性等),也就是 gc root。开始遍历对象图,标记所有可以到达的对象为存活对象,未被标记的对象则被认为是垃圾对象。进过标记后,分类成功。
  2. 垃圾查找:分类后,已经知道了对象所处的一个状态,jvm 会根据分类后对象,先找出所有垃圾对象,以便进行清理。不同的垃圾收集,其中的查找方式会产生相应的差异,随着现在 jdk 的 升级与发展,还会产生更加高效的算法,后面会有垃圾收集的算法详细介绍。
  3. 垃圾清除:标记完成后,进行最后的清理与删除。这里涉及不同的垃圾收集器,清理的方式也不同,常见的有:标记清除法、复制算法等;需要注意的是,垃圾清理可能会引起应用程序的暂停,不同的垃圾回收器通过不同的方式来减少这种暂停时间,从而提高应用程序的性能和可靠性。这也是垃圾收集器不断发展的一个重要命题。

如何判断对象是否可以被回收?

引用计数法

引用计数法(Reference Counting)是一种内存管理技术,用于跟踪对象的引用数量。每个对象都有一个引用计数器,记录着指向该对象的引用数量。

当一个对象被引用时,引用计数器加一;当一个引用被释放时,引用计数器减一。当引用计数器为零时,表示没有任何引用指向该对象,该对象可以被释放,回收其占用的内存。

可达性分析算法

可达性分析算法是JVM垃圾回收中的一种算法,它通过分析对象的引用关系,判断对象是否可达,从而决定对象是否可以被回收。

工作原理

  1. GC Roots:在Java中,GC Roots通常包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区(静态变量)中引用的对象、本地方法栈中JNI(Native方法)引用的对象等。
  2. 搜索过程:可达性分析算法从GC Roots开始,递归地访问所有可达的对象,并给它们打上标记。这个过程可以使用深度优先搜索(DFS)或广度优先搜索(BFS)等图遍历算法来实现。
  3. 回收判定:如果一个对象到GC Roots没有任何引用链相连(即该对象从GC Roots不可达),则证明该对象是不可用的,可以判定为可回收对象。

大概流程

img

我用下面的代码作为学习的例子

package cmk.study.jvm;


public class jvmStudy {

    static int add(){
        int a = 1;
        int b = 2;
        int c = (a + b)*10;
        return c;
    }

    public static void main(String[] args) throws InterruptedException {
        jvmStudy jvmStudy = new jvmStudy();
        int add1 = jvmStudy.add();
        System.out.println(add1);
    }
}

我们反编译这个class类 javap -c jvmStudy.class > jvmStudy.txt

// 编译自 "jvmStudy.java"
public class cmk.study.jvm.jvmStudy {

  // 默认构造方法
  public cmk.study.jvm.jvmStudy();
    Code:
       0: aload_0                              // 将this引用推送到操作数栈顶
       1: invokespecial #1                      // 调用父类java/lang/Object的构造方法
       4: return                               // 从构造方法返回

  // 静态方法 add,返回一个 int
  static int add();
    Code:
       0: iconst_1                             // 将常量1推送到操作数栈顶
       1: istore_0                             // 将栈顶的1存储到局部变量0
       2: iconst_2                             // 将常量2推送到操作数栈顶
       3: istore_1                             // 将栈顶的2存储到局部变量1
       4: iload_0                              // 将局部变量0的值(即1)加载到操作数栈顶
       5: iload_1                              // 将局部变量1的值(即2)加载到操作数栈顶
       6: iadd                                 // 将栈顶两值相加(1 + 2),结果3推送到操作数栈顶
       7: bipush        10                     // 将常量10推送到操作数栈顶
       9: imul                                 // 将栈顶两值相乘(3 * 10),结果30推送到操作数栈顶
      10: istore_2                             // 将栈顶的30存储到局部变量2
      11: iload_2                              // 将局部变量2的值(即30)加载到操作数栈顶
      12: ireturn                              // 返回栈顶的int值,即30

  // 主方法
  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class cmk/study/jvm/jvmStudy
          // 创建一个新的jvmStudy对象
       3: dup                                  // 复制栈顶的对象引用
       4: invokespecial #9                  // Method "<init>":()V
          // 调用构造方法初始化新创建的jvmStudy对象
       7: astore_1                             // 将对象引用存储到局部变量1
       8: aload_1                              // 加载局部变量1的对象引用到栈顶
       9: pop                                  // 弹出栈顶的对象引用,实际上这里是无用操作
      10: invokestatic  #10                 // Method add:()I
          // 调用静态方法add并将返回值推送到操作数栈顶
      13: istore_2                             // 将add方法的返回值(即30)存储到局部变量2
      14: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
          // 获取System.out静态字段(标准输出流)
      17: iload_2                              // 将局部变量2的值(即30)加载到操作数栈顶
      18: invokevirtual #20                 // Method java/io/PrintStream.println:(I)V
          // 调用PrintStream的println方法,打印int值
      21: return                               // 从main方法返回
}

反编译结果可以清楚的看到程序的执行逻辑,可以与上图做对应。

posted @ 2024-08-06 11:31  b1uesk9  阅读(7)  评论(0编辑  收藏  举报