详解JVM的底层原理

目录

1.JVM的内存区域划分

[1)程序计数器(Program Counter Register)](#1)程序计数器(Program Counter Register))

2)元数据区(Metaspace)

[3)虚拟机栈(Java Virtual Machine Stacks)](#3)虚拟机栈(Java Virtual Machine Stacks))

4)堆(Heap)

2.JVM类加载的过程

[1. 加载(Loading)](#1. 加载(Loading))

[2. 验证(Verification)](#2. 验证(Verification))

[3. 准备(Preparation)](#3. 准备(Preparation))

[4. 解析(Resolution)](#4. 解析(Resolution))

[5. 初始化(Initialization)](#5. 初始化(Initialization))

6.类加载过程示例代码

7.双亲委派模型

1)双亲委派模型的定义

2)双亲委派模型的工作流程

3.JVM的垃圾回收算法

一.找到垃圾的方法

[1)找到垃圾的方法:引用计数法(Reference Counting)](#1)找到垃圾的方法:引用计数法(Reference Counting))

原理

示例代码

优缺点

[2)找到垃圾的方法(JVM使用):可达性分析算法(Reachability Analysis)](#2)找到垃圾的方法(JVM使用):可达性分析算法(Reachability Analysis))

原理

[可作为 GC Roots 的对象](#可作为 GC Roots 的对象)

优缺点

二.垃圾回收算法

[1)标记 - 清除算法(Mark - Sweep)](#1)标记 - 清除算法(Mark - Sweep))

工作原理

优缺点

[2)标记 - 整理算法(Mark - Compact)](#2)标记 - 整理算法(Mark - Compact))

工作原理

优缺点

3)复制算法(Copying)

工作原理

优缺点

[4)分代收集算法(Generational Collection)](#4)分代收集算法(Generational Collection))

工作原理

优缺点

示例代码及说明


JVM(Java虚拟机)是Java程序运行的核心环境,它实现了Java"一次编写,到处运行"的跨平台特性。

1.JVM的内存区域划分

JVM的内存区域主要分为以下几个部分,各司其职,共同支持Java程序的运行:

程序计数器、元数据区、栈、堆。

1)程序计数器(Program Counter Register)

  • 作用 :类似于计算机组成原理中所提到的程序计数器,JVM中的程序计数器的作用也是记录当前线程正在执行的字节码指令地址

  • 特点

    • 线程私有:每个线程独立拥有一个程序计数器。

    • 唯一无OOM区域 :不会抛出OutOfMemoryError

2)元数据区**(Metaspace)**

  • 作用 :存储类元数据(如类名、方法信息、字段信息、常量池、静态变量等)。

  • 演变历史

    • JDK 7及之前:称为永久代(PermGen) ,位于堆内存中,容易引发OutOfMemoryError

    • JDK 8及之后:由**元空间(Metaspace)**替代,直接使用本地内存(Native Memory),不再受JVM堆大小限制。

  • 特点

    • 线程共享:所有线程共享元数据区。

    • 动态扩展 :默认不限制大小,可通过-XX:MaxMetaspaceSize设置上限。

    • 异常OutOfMemoryError(当本地内存不足时抛出)。

3)虚拟机栈(Java Virtual Machine Stacks)

  • 作用 :存储线程的方法调用信息 ,每个方法对应一个栈帧(Stack Frame)

  • 栈帧内容

    • 局部变量表:保存方法的参数和局部变量。

    • 操作数栈:执行字节码指令时的临时操作数存储区。

    • 动态链接:指向方法区中的方法引用。

    • 方法返回地址:记录方法执行完毕后返回的位置。

  • 特点

    • 线程私有:每个线程独立拥有一个栈。

    • 异常StackOverflowError(栈深度超出限制)或OutOfMemoryError(扩展失败)。

4)堆(Heap)

  • 作用 :存放所有对象实例和数组,是垃圾回收(GC)的主要区域。

  • 特点

    • 线程共享:所有线程均可访问堆中的对象。

    • 细分结构

      • 新生代(Young Generation):分为Eden区、Survivor区(From/To),存放新创建的对象。

      • 老年代(Old Generation):长期存活的对象(经过多次GC后存活)晋升至此。

    • 异常OutOfMemoryError(当堆内存无法分配时抛出)。

2.JVM类加载的过程

1. 加载(Loading)

这是类加载的第一个阶段,在这个阶段,JVM 会完成以下操作:

  • 通过类的全限定名来获取定义此类的二进制字节流。这一步可以从多种来源获取字节流,比如本地文件系统、网络、ZIP 包等。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2. 验证(Verification)

此阶段的目的是确保被加载的类的字节流符合 JVM 规范,不会危害 JVM 自身的安全。验证主要包含以下几个方面:

  • 文件格式验证 :验证字节流是否符合 Class 文件格式的规范,比如是否以 0xCAFEBABE 开头、主次版本号是否在当前 JVM 支持的范围内等。
  • 元数据验证 :对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,例如这个类是否有父类(除了 java.lang.Object 之外,所有的类都应该有父类)。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如,对方法体进行校验,确保跳转指令不会跳转到方法体以外的字节码指令上。
  • 符号引用验证:在解析阶段中,会将符号引用转换为直接引用,符号引用验证是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,比如符号引用所涉及的类是否能被找到等。

3. 准备(Preparation)

该阶段是为类的静态变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的初始值通常是数据类型的零值,例如:

java 复制代码
public class PrepareExample {
    public static int value = 123;
}

在准备阶段,value 变量会被初始化为 0,而不是 123。把 value 赋值为 123 的操作是在初始化阶段完成的。

4. 解析(Resolution)

解析 阶段是 JVM 将常量池内的符号引用 替换为直接引用的过程。

符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

5. 初始化(Initialization)

这是类加载的最后一个阶段,在这个阶段,JVM 才真正开始执行类中定义的 Java 程序代码(或者说是字节码)。

初始化阶段是执行类构造器 <clinit>() 方法的过程。<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的。例如:

java 复制代码
public class InitializationExample {
    static {
        System.out.println("静态代码块执行");
    }
    public static int value = 123;
}

在初始化阶段,静态代码块会被执行,并且 value 变量会被赋值为 123。

6.类加载过程示例代码

java 复制代码
class Parent {
    static {
        System.out.println("Parent static block");
    }
    public static int parentValue = 10;
}

class Child extends Parent {
    static {
        System.out.println("Child static block");
    }
    public static int childValue = 20;
}

public class ClassLoadingExample {
    public static void main(String[] args) {
        System.out.println(Child.childValue);
    }
}

在上述代码中,当执行 main 方法时 ,会触发 Child 类的加载

由于 Child 类继承自 Parent 类,所以会先加载 Parent,依次经过加载、验证、准备、解析和初始化阶段。

在初始化阶段,会执行 Parent 类的静态代码块,将 parentValue 赋值为 10。接着加载 Child 类,同样经过这些阶段,执行 Child 类的静态代码块,将 childValue 赋值为 20。最后打印出 Child.childValue 的值。

7.双亲委派模型

双亲委派模型与 JVM(Java Virtual Machine)有着紧密且多方面的联系,在 JVM 的类加载体系中扮演着至关重要的角色。

1)双亲委派模型的定义

双亲委派模型规定了类加载器之间的层次关系和类加载的委托机制。在 Java 中,存在多种类加载器,它们形成了一个树形结构,从顶层到底层主要有以下几种:

  • 启动类加载器(Bootstrap ClassLoader) :由 C++ 实现,是最顶层的类加载器,负责加载 Java 的核心类库,如 java.lang 包下的类。
  • 扩展类加载器(Extension ClassLoader) :由 Java 实现,负责加载 JAVA_HOME/lib/ext 目录下的类库。
  • 应用程序类加载器(Application ClassLoader) :也由 Java 实现,负责加载用户类路径(classpath)上的类库,一般情况下,我们自己编写的 Java 类就是由该类加载器加载的。
  • 自定义类加载器(User-Defined ClassLoader) :用户可以根据需求自定义类加载器,继承自 java.lang.ClassLoader 类。

2)双亲委派模型的工作流程

当一个类加载器收到类加载请求时 ,它不会立即尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。

因此所有的加载请求 最终都会传送顶层的启动类加载器中。

只有当父类加载器 反馈自己无法完成 这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

具体流程如下:

  1. 应用程序类加载器收到类加载请求。
  2. 应用程序类加载器将请求委派给扩展类加载器。
  3. 扩展类加载器将请求委派给启动类加载器。
  4. 启动类加载器在其搜索范围内查找该类,如果找到则加载该类;如果找不到,则将请求返回给扩展类加载器。
  5. 扩展类加载器在其搜索范围内查找该类,如果找到则加载该类;如果找不到,则将请求返回给应用程序类加载器。
  6. 应用程序类加载器在其搜索范围内查找该类,如果找到则加载该类;如果找不到,则抛出 ClassNotFoundException 异常。

3.JVM的垃圾回收算法

JVM(Java Virtual Machine)的垃圾回收算法旨在自动回收不再使用的内存,以保证系统的性能和稳定性。那么首先,JVM如何找到垃圾呢?

准确找出垃圾对象(即不再被使用、可回收内存的对象)是垃圾回收的关键前提。

一.找到垃圾的方法

1)找到垃圾的方法:引用计数法(Reference Counting)

原理

给每个对象配备一个引用计数器,每当有一个地方引用该对象时,计数器的值就加 1;当引用失效时,计数器的值就减 1。

要使用一个对象,一定是通过引用来完成的

当计数器的值为 0 时,就表明这个对象不再被引用 ,可被视为垃圾对象

示例代码
python 复制代码
# Python 示例模拟引用计数
class MyObject:
    def __init__(self):
        pass

# 创建对象
obj1 = MyObject()
obj2 = obj1  # 引用计数加 1
obj1 = None  # 引用计数减 1
obj2 = None  # 引用计数减 1,此时对象可被回收
优缺点
  • 优点:实现简单,判定效率高,能及时发现垃圾对象。
  • 缺点:难以处理循环引用的情况。比如两个对象相互引用,即使它们在程序中已不再被其他地方使用,由于引用计数器的值不为 0,它们也不会被当作垃圾回收。

2)找到垃圾的方法(JVM使用):可达性分析算法(Reachability Analysis)

原理

以一系列被称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。

当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 到这个对象不可达 ),则证明此对象是不可用 的,可被判定为垃圾对象

可作为 GC Roots 的对象
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象:例如,在方法中创建的局部变量所引用的对象。
  • 方法区中类静态属性引用的对象:像类的静态变量引用的对象。
  • 方法区中常量引用的对象 :例如,使用 final 关键字定义的常量所引用的对象。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象:也就是在本地方法中引用的对象。
java 复制代码
public class ReachabilityAnalysisExample {
    public static void main(String[] args) {
        Object obj1 = new Object(); // obj1 是 GC Roots 可达的
        Object obj2 = new Object();
        obj1 = null; // obj1 不再是 GC Roots 可达的,可能被回收
        obj2 = null; // obj2 不再是 GC Roots 可达的,可能被回收
    }
}
优缺点
  • 优点 :能够有效解决引用计数法中循环引用的问题,是目前主流 JVM 采用的判定垃圾对象的方法。
  • 缺点:实现相对复杂,需要进行递归遍历,并且在进行可达性分析时,需要暂停所有的用户线程(即 "Stop The World"),这可能会对应用程序的性能产生一定影响。

二.垃圾回收算法

JVM(Java Virtual Machine)的垃圾回收算法旨在自动回收不再使用的内存,以保证系统的性能和稳定性。

下面详细介绍几种常见的垃圾回收算法。

1)标记 - 清除算法(Mark - Sweep)

工作原理

该算法分为两个阶段。

首先是标记阶段,垃圾回收器会从根对象(如栈中的引用、静态变量等)开始遍历,标记出所有存活的对象。

接着是清除阶段,对未被标记的对象(即垃圾对象)进行清除,释放其所占用的内存空间。

优缺点
  • 优点:实现简单,不需要额外的数据结构来记录内存信息。
  • 缺点:容易产生内存碎片。随着多次回收,内存中会出现大量不连续的小内存块,当需要分配较大对象时,可能会因找不到足够大的连续内存空间而提前触发新的垃圾回收。

2)标记 - 整理算法(Mark - Compact)

工作原理

同样先进行标记阶段,找出所有存活的对象。

之后进入整理阶段 ,将存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

优缺点
  • 优点:解决了标记 - 清除算法产生内存碎片的问题,使内存空间连续,有利于大对象的分配。
  • 缺点:整理过程需要移动对象,会带来一定的性能开销,且在移动对象时需要暂停应用程序的执行。

3)复制算法(Copying)

工作原理

将可用内存按容量划分为大小相等的两块每次只使用其中一块

当这一块内存用完了,就将还存活的对象 复制到另外一块上面,然后把已使用过的内存空间一次清理掉。

优缺点
  • 优点:实现简单,回收效率高,且不会产生内存碎片。
  • 缺点:可用内存空间减少为原来的一半,内存利用率较低。通常适用于对象存活率较低的场景,如新生代。

4)分代收集算法(Generational Collection)

工作原理

根据对象的存活周期将内存划分为不同的区域,一般分为新生代老年代

新生代 中对象的存活时间较短,大部分对象很快就会变成垃圾,因此适合使用复制算法

老年代 中对象的存活时间较长,对象存活率较高,采用标记 - 清除标记 - 整理算法更为合适。

优缺点
  • 优点:结合了不同算法的优势,根据对象的特点选择合适的算法,提高了垃圾回收的效率。
  • 缺点:需要对内存进行分代管理,增加了系统的复杂性。

示例代码及说明

以下是一个简单的 Java 代码示例,展示了对象创建和垃圾回收的情况:

java 复制代码
public class GarbageCollectionExample {
    public static void main(String[] args) {
        // 创建大量对象
        for (int i = 0; i < 100000; i++) {
            new Object();
        }
        // 手动触发垃圾回收
        System.gc();
    }
}

在上述代码中,通过循环创建了大量的 Object 对象,这些对象大部分会很快成为垃圾。System.gc() 方法用于手动触发垃圾回收,实际应用中,JVM 会根据内存使用情况自动触发垃圾回收。

不同的垃圾回收算法会在这个过程中发挥作用,以确保内存的有效利用。

相关推荐
菜就多练吧1 小时前
JVM 内存分布详解
java·开发语言·jvm
碎梦归途6 小时前
23种设计模式-结构型模式之外观模式(Java版本)
java·开发语言·jvm·设计模式·intellij-idea·外观模式
恶语伤人六月寒7 小时前
深⼊理解 JVM 执⾏引擎
jvm
提高记忆力8 小时前
JVM学习
jvm
Java知识库10 小时前
Java BIO、NIO、AIO、Netty面试题(已整理全套PDF版本)
java·开发语言·jvm·面试·程序员
Geek__199210 小时前
Sqlite3交叉编译全过程
jvm·数据库·sqlite
碎梦归途12 小时前
23种设计模式-结构型模式之代理模式(Java版本)
java·开发语言·jvm·设计模式·代理模式
{⌐■_■}15 小时前
【go】什么是Go语言中的GC,作用是什么?调优,sync.Pool优化,逃逸分析演示
java·开发语言·javascript·jvm·数据库·后端·golang
碎梦归途16 小时前
23种设计模式-创建型模式之建造者模式(Java版本)
java·开发语言·jvm·设计模式·intellij-idea·建造者模式