JVM知识汇总

JVM 内存区域划分

JVM 在运行 Java 程序时,会将内存划分为多个不同的区域,不同区域负责存放不同类型的数据。

一. 程序计数器(Program Counter Register)

程序计数器是一块线程私有 的内存区域,用于保存当前线程下一条要执行的指令地址

  • 这里的"指令"指的是 Java 字节码指令
  • 当线程切换时,程序计数器可以保证线程恢复到正确的执行位置

特点:

  • 线程私有
  • 内存空间很小
  • 是 JVM 中唯一一个不会发生 OutOfMemoryError 的区域

二. 堆(Heap)

堆是 JVM 中最大的一块内存区域,用于存放对象实例。

  • 通过 new 关键字创建的对象,通常存放在堆中
  • 数组对象也存放在堆中

特点:

  • 线程共享
  • 几乎所有对象和数组都在堆中分配
  • 是垃圾回收(GC)最主要的管理区域
  • 最容易发生内存溢出异常

常见异常:

  • OutOfMemoryError: Java heap space

三. 栈(Stack)

栈是线程私有 的内存区域,用于存储方法调用相关的信息

当一个方法被调用时,会创建一个栈帧(Stack Frame),方法执行结束后,对应的栈帧出栈。

栈中主要保存的内容包括:

  • 局部变量(基本数据类型、对象引用)
  • 方法的形参
  • 方法之间的调用关系
  • 操作数栈
  • 方法入口,返回地址

3.1 Java 虚拟机栈

Java 虚拟机栈用于保存 Java 方法执行时的调用关系和运行数据

  • 每个 Java 方法对应一个栈帧
  • 方法调用遵循 后进先出(LIFO) 的规则

常见异常:

  • StackOverflowError(递归过深或死递归)

3.2 本地方法栈

本地方法栈用于支持 JVM 调用 本地方法(Native Method) ,这些方法通常由 C / C++ 实现

  • 本地方法栈保存的是本地方法的调用关系
  • 在 JVM 内部实现,与操作系统和底层库交互

特点:

  • 线程私有
  • 结构与 Java 虚拟机栈类似
  • 也可能抛出 StackOverflowError

四. 元数据区(方法区 / Metaspace)

元数据区用于存储类的元数据信息,包括:

  • 类的结构信息(类名、父类、接口等)
  • 方法信息
  • 运行时常量池
  • 静态变量(static 修饰的变量)

版本说明

  • JDK 7 及之前:方法区的实现是 永久代(PermGen)
  • JDK 8 及之后:方法区由 元空间(Metaspace) 实现,使用的是本地内存

常见异常:

  • OutOfMemoryError: Metaspace

什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执⾏时间的⽅式来实现,因此在任何⼀个确定的

时刻,⼀个处理器(多核处理器则指的是⼀个内核)都只会执⾏⼀条线程中的指令。因此为了切换线程后

能恢复到正确的执⾏位置,每条线程都需要独⽴的程序计数器,各条线程之间计数器互不影响,独⽴

存储。我们就把类似这类区域称之为"线程私有"的内存。

总结

内存区域 是否线程私有 主要存储内容
程序计数器 当前线程执行的字节码指令地址
对象实例,成员变量
Java 虚拟机栈 Java 方法调用与局部变量
本地方法栈 Native 方法调用
元数据区(方法区) 类信息、常量、静态变量

JVM类加载的过程

在学习 JVM 的过程中,类加载机制 是一个绕不开的核心知识点。

很多 JVM 相关的问题,比如静态变量初始化顺序、类什么时候被加载、为什么会出现某些奇怪的空指针问题,本质都和类加载过程有关。

本文将从整体流程出发,用通俗易懂的方式讲解 JVM 的类加载过程。


一、什么是 JVM 类加载

JVM 类加载,指的是 JVM 将 .class 文件加载到内存,并最终形成可以被程序使用的 Class 对象的过程

简单来说,就是把"磁盘上的类文件",变成"内存中可运行的类"。


二、类加载的整体流程

JVM 的类加载过程一共分为五个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)

其中,验证、准备、解析 三个阶段合称为 连接(Linking)阶段

整体流程如下:

加载 → 验证 → 准备 → 解析 → 初始化


三、加载(Loading)

1. 加载阶段做了什么?

加载阶段主要完成三件事情:

  • 根据类的全限定名获取对应的字节码文件
  • 将字节码文件转换为 JVM 内部的数据结构
  • 在内存中生成一个代表该类的 Class 对象

2. 加载阶段的特点

  • 类的来源可以多种多样(本地文件、网络、Jar 包等)
  • 加载由 **类加载器(ClassLoader)**完成
  • 该阶段不会执行任何 Java 代码

四、验证(Verification)

1. 验证阶段的作用

验证阶段的主要目的是 确保字节码文件是合法、安全的,防止恶意代码破坏 JVM。

验证内容包括:

  • 文件格式是否正确
  • 字节码是否符合 JVM 规范
  • 是否存在非法指令或越界访问

2. 验证失败的结果

如果验证失败,类加载过程会直接终止,程序无法继续运行。


五、准备(Preparation)

1. 准备阶段做了什么?

准备阶段主要完成:

  • 类变量(static 变量)分配内存
  • 将类变量设置为 默认初始值

注意,这里的初始化只是 默认值初始化,并不是程序中写的赋值。

2. 常见误区

很多人误以为在准备阶段就会执行静态变量的赋值操作,其实这是错误的。

真正的赋值发生在 初始化阶段


六、解析(Resolution)

1. 解析阶段的作用

解析阶段是将 符号引用转换为直接引用

简单理解就是:

  • 将类名、方法名、字段名等"符号"
  • 替换为 JVM 可以直接定位的内存地址

2. 解析的特点

  • 解析阶段不一定一次性完成
  • JVM 可能会在运行过程中延迟解析

七、初始化(Initialization)

1. 初始化阶段做了什么?

初始化阶段是 类加载过程中最重要的阶段 ,也是 唯一会执行 Java 代码的阶段

主要执行内容包括:

  • 为静态变量赋程序中定义的值
  • 执行静态代码块

2. 初始化顺序规则

初始化遵循以下顺序:

  • 父类先初始化
  • 子类再初始化
  • 同一个类中,按照代码书写顺序执行

八、类什么时候会被初始化?

只有在 主动使用类 时,才会触发类的初始化。常见场景包括:

  • 创建类的实例
  • 访问或修改类的静态变量
  • 调用类的静态方法
  • 通过反射使用类
  • JVM 启动时指定的主类

以下情况不会触发初始化:

  • 定义类的数组
  • 引用常量

九、类加载过程总结

阶段 主要作用 是否执行 Java 代码
加载 获取字节码并生成 Class 对象
验证 保证字节码合法安全
准备 分配 static 变量内存并赋默认值
解析 符号引用转为直接引用
初始化 执行静态变量赋值和静态代码块

JVM 双亲委派模型详解

一、什么是双亲委派模型

双亲委派模型(Parent Delegation Model)是指:
当一个类加载器接收到类加载请求时,优先将该请求委派给父类加载器去完成,只有当父类加载器无法加载时,子加载器才会尝试自己加载。

简单概括就是一句话:

先交给父加载器,父加载器不行,自己再加载。


二、为什么要使用双亲委派模型

双亲委派模型的设计主要是为了解决两个核心问题:

1. 保证 Java 核心类的安全性

Java 的核心类库(如 java.langjava.util 等)是 Java 运行的基础。

如果没有双亲委派模型:

  • 用户可以自定义与核心类同名的类
  • JVM 可能会加载错误的类
  • 会对 Java 的安全性造成严重威胁

通过双亲委派模型:

  • 核心类始终由最顶层的类加载器加载
  • 用户无法替换或篡改 Java 核心类

2. 保证类的唯一性

在 JVM 中,一个类是否相同,不仅取决于类的全限定名,还取决于 加载它的类加载器

双亲委派模型可以避免:

  • 同一个类被多个类加载器重复加载
  • 造成类冲突和类型不一致的问题

从而保证类在 JVM 中的唯一性。


三、JVM 中的主要类加载器

在 JVM 中,类加载器通常分为三层结构,自上而下分别是:

1. 启动类加载器(Bootstrap ClassLoader)

  • 最顶层的类加载器
  • 负责加载 Java 最核心的类库
  • 通常加载 java.langjava.util 等基础类
  • 由 JVM 使用本地代码实现

2. 扩展类加载器(Extension ClassLoader)

  • 负责加载 Java 的扩展类库
  • 是启动类加载器的子加载器
  • 用于扩展 Java 的标准功能

3. 应用程序类加载器(Application ClassLoader)

  • 负责加载应用程序中的类
  • 即开发者自己编写的类
  • 是日常开发中最常用的类加载器

四、双亲委派模型的工作流程

当应用程序类加载器需要加载某个类时,整体流程如下:

  1. 应用程序类加载器将请求委派给扩展类加载器
  2. 扩展类加载器继续将请求委派给启动类加载器
  3. 启动类加载器尝试加载该类
  4. 如果启动类加载器无法加载,请求返回给扩展类加载器
  5. 如果扩展类加载器仍然无法加载,最终由应用程序类加载器自己加载

可以总结为:

自下而上委派,自上而下尝试加载。


五、双亲委派模型与类加载过程的关系

在 JVM 类加载的五个阶段中:

  • 加载阶段由类加载器完成
  • 双亲委派模型正是类加载器在加载阶段遵循的规则

两者的区别在于:

  • 类加载过程描述的是"类加载分为哪些阶段"
  • 双亲委派模型描述的是"加载阶段如何选择类加载器"

六、双亲委派模型是否可以被打破

理论上,双亲委派模型是可以被打破的,但一般不建议这样做。

在某些特殊场景下,例如:

  • 自定义类加载器
  • 模块隔离需求
  • 容器或框架级别的加载控制

一些框架(如 Tomcat、SPI 机制)会在特定场景下打破双亲委派模型,以满足实际需求。

但在普通业务开发和面试中,默认都是遵循双亲委派模型


七、双亲委派模型的优点

  1. 避免重复加载类:比如 A 类和 B 类都有⼀个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那
    么在 B 类进行加载时就不需要在重复加载 C 类了。
  2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模
    型,而是每个类加载器加载自己的话就会出现⼀些问题,比如我们编写⼀个称为 java.lang.Object
    类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自
    己提供的因此安全性就不能得到保证了。

JVM 内存回收与垃圾回收器

一、垃圾回收的意义与范围

在 JVM 的运行区域中,程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而灭,内存分配具有确定性,不需要过多考虑回收。

垃圾回收的核心关注点是:Java 堆(Heap)和方法区(Method Area)。这两个区域的内存分配和回收是动态的,几乎所有的对象实例都存放在堆中。

二、判断对象是否存活的逻辑

1. 引用计数算法

  • 原理:为对象添加一个引用计数器。每当有一个地方引用它,计数器加 1;引用失效时,计数器减 1。计数器为 0 的对象被视为"垃圾"。
  • 局限性:它无法解决循环引用的问题。例如对象 A 引用对象 B,对象 B 也引用对象 A,但此外再无其他引用。此时两者的计数器都不为 0,导致无法被回收。因此,主流 JVM(如 HotSpot)并不采用此算法。

2. 可达性分析算法

  • 原理:以一系列被称为 GC Roots 的对象作为起点,从这些节点开始向下搜索。搜索走过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链相连(即不可达),则证明此对象不可用。
  • 可作为 GC Roots 的对象包括
    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中类静态属性引用的对象。
    3. 方法区中常量引用的对象。
    4. 本地方法栈中 JNI(即 Native 方法)引用的对象。

三、强、软、弱、虚:引用的四种境界

为了支持更精细的内存管理,Java 将引用分成了四类:

  • 强引用 :最普遍的引用,如 Object obj = new Object()。只要强引用存在,垃圾回收器永远不会回收被引用的对象。
  • 软引用:描述还有用但非必需的对象。在系统将要发生内存溢出(OOM)之前,会把这些对象列入回收范围进行第二次回收。如果这次回收后内存仍不足,才会抛出溢出异常。
  • 弱引用:强度比软引用更弱。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。无论当前内存是否足够,只要发生 GC,弱引用对象就会被回收。
  • 虚引用:最弱的一种。它不影响对象的生存时间,也无法通过虚引用获取对象实例。设置虚引用的唯一目的是在对象被回收时收到一个系统通知。

四、垃圾回收算法(方法论)

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

  • 过程:分为标记和清除两个阶段。首先标记出所有需要回收的对象,标记完成后统一回收。
  • 缺点:效率低;产生大量不连续的内存碎片。碎片过多会导致后续大对象无法找到连续内存而频繁触发 GC。

2. 复制算法 (Copying)

  • 过程:将可用内存划分为大小相等的两块,每次只使用一块。当这块内存用完时,将还活着的对象复制到另一块,然后清理掉当前这一块。
  • 优点:实现简单,运行高效,无碎片。
  • 应用:现代 JVM 用此算法回收新生代。为了提高空间利用率,HotSpot 将新生代分为 Eden、Survivor From、Survivor To 三部分(比例 8:1:1)。

3. 标记-整理算法 (Mark-Compact)

  • 过程:标记过程与标记-清除一致,但后续步骤是让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。
  • 优点:解决了碎片问题,不需要浪费一半的空间。
  • 应用:主要用于老年代。

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

  • 原理:根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代。
  • 策略:新生代中对象死得快,采用复制算法;老年代中对象存活率高,采用标记-清除或标记-整理算法。

五、垃圾收集器(具体实现)

1. 经典收集器

  • Serial 收集器:单线程。进行垃圾回收时,必须暂停其他所有工作线程(Stop The World,简称 STW)。
  • ParNew 收集器:Serial 的多线程版本,常与 CMS 配合使用。
  • Parallel Scavenge 收集器:关注吞吐量(CPU 运行用户代码时间与总消耗时间的比值),适合后台计算。
  • CMS 收集器 (Concurrent Mark Sweep):以最短停顿时间为目标。过程包括:初始标记(STW)、并发标记、重新标记(STW)、并发清除。它有碎片多、无法处理浮动垃圾的缺点。

2. G1 收集器 (Garbage First)

  • 地位:面向服务端应用,旨在替换 CMS。
  • 原理:将整个堆划分为多个大小相等的独立区域(Region)。它依然保留新生代和老年代的概念,但它们不再是物理隔离的。
  • 优势:可预测的停顿时间模型。它会根据每个 Region 的回收价值(回收所获得的空间大小以及回收所需时间)维护一个优先级列表,优先回收价值最大的 Region。

六、对象的一生:内存分配与晋升历程

  1. 新生诞生:绝大多数对象在 Eden 区出生。
  2. 初次历练 (Minor GC):当 Eden 区空间不足时,触发 Minor GC。存活对象被复制到 Survivor From 区,年龄设为 1。
  3. 来回搬迁:在后续的 Minor GC 中,存活对象在两个 Survivor 区(From 和 To)之间往返复制。每经过一次 GC,年龄加 1。
  4. 晋升老人:当对象年龄达到 15 岁(默认值)时,会被移动到老年代。大对象(需要大量连续内存空间的对象)也会直接进入老年代。
  5. 寿终正寝 (Full GC) :当老年代空间不足时,会触发 Full GC。如果 Full GC 后内存依然不足,就会抛出 OutOfMemoryError

相关推荐
CaracalTiger9 小时前
如何解决Unexpected token ‘<’, “<!doctype “… is not valid JSON 报错问题
java·开发语言·jvm·spring boot·python·spring cloud·json
江湖有缘17 小时前
自托管RSS解决方案:Docker化Fusion安装教程
java·jvm·docker
Chan1620 小时前
《深入理解Java虚拟机》| 类加载与双亲委派机制
java·开发语言·jvm·面试·java-ee·intellij-idea
闻哥1 天前
GET和POST请求的本质区别
java·网络·jvm·spring·http·面试·https
野犬寒鸦2 天前
从零起步学习并发编程 || 第八章:ThreadLocal深层解析及常见问题:避坑指南与最佳实践
java·服务器·开发语言·jvm·算法
heartbeat..2 天前
Java 中的类加载器的双亲委派模型:原理、层级与实现
java·开发语言·jvm·类加载器
Mr Aokey3 天前
JVM三剑客:内存模型、类加载机制与垃圾回收精讲
java·jvm
程序员ken3 天前
献给自己的一款个人管理的桌面软件(一)
java·开发语言·jvm
团子的二进制世界3 天前
JVM 运行时数据区的 7 大组成部分
jvm