JVM 全体系深度解析笔记

第一部分:JVM 宏观认知与面试必答

P1 1、JVM 的学习方式

1.1 学习核心原则
  • 先体系后细节:先掌握 JVM 整体架构,再深入每个模块的原理,避免碎片化学习
  • 理论 + 实践结合:理解原理的同时,用工具验证、写代码复现问题(如 OOM、GC 日志)
  • 面向面试与生产:重点掌握高频面试点(类加载、GC、内存调优),同时结合生产环境调优场景
  • 循序渐进:从基础架构 → 内存区域 → 类加载 → 垃圾回收 → 调优工具,逐步深入
1.2 学习路径规划
  1. 基础阶段:掌握 JVM 体系结构、内存区域划分、类加载机制
  2. 核心阶段:深入垃圾回收算法、垃圾收集器、GC 日志分析
  3. 实战阶段:学习 JProfiler 等调优工具,解决 OOM、内存泄漏、性能优化问题
  4. 进阶阶段:理解 JVM 底层实现、即时编译、虚拟机优化等高级特性
1.3 学习资源推荐
  • 书籍:《深入理解 Java 虚拟机》(周志明,JVM 经典圣经)、《Java 虚拟机规范》
  • 工具:JProfiler、VisualVM、jstat、jmap、jhat 等 JDK 自带工具
  • 实践:写代码复现 OOM、分析 GC 日志、模拟内存泄漏场景

1. 请你谈谈你对 JVM 的理解?Java8 虚拟机和之前的变化更新?

1.1 JVM 核心定义(一句话回答)

JVM(Java Virtual Machine)是运行在操作系统之上的软件模拟计算机 ,负责加载、执行 Java 字节码,实现跨平台 (一次编写,到处运行)。它屏蔽了底层系统差异,提供自动内存管理(GC)安全沙箱机制。

1.2 JVM 的核心作用
核心作用 说明
字节码执行引擎 解释执行字节码,或通过 JIT 编译为机器码执行
内存管理系统 分配、使用、回收内存(堆、栈、方法区)
垃圾回收器 (GC) 自动回收无引用对象,释放内存
安全沙箱 限制程序对系统资源的访问,保障安全
1.3 Java 8 虚拟机核心变化(面试必点)

Java 8 是 JVM 历史的分水岭,核心变化如下:

  1. 元空间(Metaspace)替代永久代(PermGen)
    • JDK 7 及之前 :方法区 / 运行时常量池位于永久代 (堆内存),受 -XX:PermSize 限制,易 OOM。
    • JDK 8 :永久代被移除,方法区迁移到元空间(直接使用本地内存),大小动态扩展,解决永久代 OOM 问题。
  2. 内置 Nashorn JavaScript 引擎(已被替代)
  3. 默认垃圾收集器变更
    • JDK 7:默认 Serial GC。
    • JDK 8 :默认 Parallel GC(高吞吐量),同时引入 G1 GC 作为主流选择。
  4. 引入 Lambda 表达式(依赖 JVM invokedynamic 指令)
  5. 时区数据独立:JRE 不再包含时区数据,需独立依赖。
1.4 从 Java 8 到现代版本(Java 17)的进阶
版本 核心 JVM 变化
Java 9 模块化系统(Project Jigsaw),JRE 被废弃
Java 11 引入 ZGC(低延迟垃圾收集器),正式版
Java 17 引入 Shenandoah GC,正式版;密封类;彻底移除 Applet

2. 什么是 OOM,什么是栈溢出 StackOverflowError?怎么分析?

2.1 OOM(OutOfMemoryError)定义

OOM 是内存溢出错误 ,指 JVM 在申请内存时,没有足够的空闲内存可用,且垃圾回收无法释放更多内存,导致程序崩溃。

  • 核心特征:JVM 堆内存耗尽、方法区耗尽、直接内存耗尽等。
2.2 StackOverflowError(栈溢出)定义

栈溢出是线程请求的栈深度超过了虚拟机允许的最大深度 ,通常由无限递归方法调用链过长导致。

  • 核心区别 :OOM 是堆 / 方法区 内存不够;StackOverflowError 是线程栈深度不够。
2.3 分析方法(实战步骤)
2.3.1 OOM 分析流程
  1. 获取 Dump 文件 :配置 JVM 参数开启快照。

    复制代码
    # 自动生成OOM时的dump文件
    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof
  2. 使用分析工具 :导入hprof文件。

    • 工具:JProfiler、VisualVM、MAT(Eclipse Memory Analyzer)。
  3. 定位问题

    • 查看大对象:哪些实例占用内存最多。
    • 查看引用链:找出哪些引用未释放(如静态 Map、未关闭的连接)。
2.3.2 StackOverflowError 分析流程
  1. 查看堆栈信息:查看异常栈追踪,找到递归入口或无限循环方法。
  2. 优化代码
    • 修复无限递归。
    • 优化递归为迭代。
    • 调整栈大小(-Xss,不推荐)。

3. JVM 的常用调优参数有哪些?

3.1 堆内存核心参数(最常用)
参数 作用 说明
-Xms 初始堆大小 等价于-XX:InitialHeapSize,建议与-Xmx一致
-Xmx 最大堆大小 等价于-XX:MaxHeapSize,物理内存的 1/4~1/2
-Xmn 新生代大小 -Xmn2g,默认占堆的 1/3
-Xss 栈大小 每个线程的栈大小,默认 1M,减小可提升线程数
3.2 垃圾收集器参数
参数 作用
-XX:+UseG1GC 使用 G1 收集器(主流推荐)
-XX:+UseParallelGC 使用 Parallel GC(默认)
-XX:+UseConcMarkSweepGC 使用 CMS 收集器(老年代)
3.3 元空间参数(Java 8+)
参数 作用
-XX:MetaspaceSize 元空间初始大小
-XX:MaxMetaspaceSize 元空间最大大小
3.4 内存快照参数
参数 作用
-XX:+HeapDumpOnOutOfMemoryError OOM 时自动生成 Dump 文件
-XX:HeapDumpPath 指定 Dump 文件生成路径

4. 内存快照如何抓取,怎么分析 Dump 文件?

4.1 内存快照(Dump)抓取方式
4.1.1 自动抓取(生产环境推荐)

配置 JVM 启动参数,OOM 发生时自动抓取:

复制代码
java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./app.hprof app.jar
4.1.2 手动抓取(在线 / 离线)

使用 JDK 自带工具:

复制代码
# 生成整个堆快照
jmap -dump:format=b,file=heap.hprof <pid>

# 生成存活对象快照(推荐,体积小)
jmap -dump:live,format=b,file=live.hprof <pid>
4.2 Dump 文件分析步骤(核心流程)
  1. 工具选择:MAT(功能最强)、JProfiler、VisualVM。
  2. 核心分析动作
    • Dominator Tree(支配树):找出占用内存最大的对象。
    • Histogram(直方图):统计各类实例数量。
    • GC Roots:查找对象的引用链,确定为何未被回收。
  3. 常见结论
    • 大集合未清理:缓存 Map 无限增长。
    • 数据库连接未关闭:导致连接池溢出。
    • ThreadLocal 未清理:线程复用导致内存泄漏。

5. 谈谈 JVM 中,类加载器你的认识?

5.1 类加载器定义

类加载器(ClassLoader)是 JVM 的类加载子系统 核心,负责将.class文件的二进制字节流加载到 JVM 中,生成java.lang.Class对象。

5.2 分类与层级(双亲委派)

JVM 采用双亲委派机制,层级从下到上如下:

  1. 启动类加载器(Bootstrap ClassLoader) :C++ 实现,加载rt.jar核心类。
  2. 扩展类加载器(Extension ClassLoader) :加载jre/lib/ext扩展类。
  3. 应用程序类加载器(Application ClassLoader):加载用户 classpath 下的类。
  4. 自定义类加载器 :继承ClassLoader,实现自定义加载逻辑(如热部署、加密类加载)。
5.3 双亲委派机制(核心面试点)
  • 核心规则 :当一个类加载器收到加载请求时,优先委托父类加载器加载,父类无法加载时,才由自己加载。
  • 优势
    1. 避免类重复加载 :保证核心类(如Object)只加载一次。
    2. 安全性保障 :防止恶意代码替换核心类(如自定义java.lang.String)。
  • 破坏场景:SPI 机制(如 JDBC 驱动)、热部署、OSGi 架构。

P9 9、使用 JProfiler 工具分析 OOM 原因

9.1 JProfiler 简介

JProfiler 是一款Java 性能分析工具,用于分析内存泄漏、OOM、CPU 性能瓶颈、线程问题等,是 Java 调优的必备工具。

9.2 JProfiler 分析 OOM 的步骤
9.2.1 准备工作
  • 启动 JProfiler,连接目标 Java 进程(本地 / 远程)
  • 配置 OOM 触发条件,开启内存快照
9.2.2 分析内存快照
  1. 查看堆内存使用情况:分析堆内存的占用率、对象数量
  2. 找出大对象:按对象大小排序,找出占用内存最多的对象(如大集合、缓存)
  3. 分析对象引用链:查看对象的引用关系,找出内存泄漏的根源(如未关闭的连接、静态集合)
  4. 分析 GC 情况:查看 GC 频率、GC 时间,找出 GC 瓶颈
9.3 常见 OOM 原因与解决方案
OOM 类型 原因 解决方案
堆 OOM 对象无限创建、内存泄漏、堆大小过小 优化代码、增大堆大小、修复内存泄漏
元空间 OOM 类加载过多、动态代理过多、元空间过小 增大元空间大小、优化类加载、避免动态代理滥用
栈 OOM 方法递归过深、方法调用链过长 优化递归逻辑、增大栈大小
直接内存 OOM NIO 使用过多、直接内存大小过小 增大直接内存大小、优化 NIO 使用

P14 14、如何快速学习方法讲解

14.1 快速学习 JVM 的核心方法
1. 建立体系化认知
  • 先画 JVM 体系结构图,掌握整体架构,再逐个模块深入
  • 按照体系结构 → 内存区域 → 类加载 → 垃圾回收 → 调优工具的顺序学习
2. 抓重点,避细节
  • 重点掌握高频面试点:类加载、双亲委派、内存区域、GC 算法、垃圾收集器、OOM 分析
  • 非核心细节(如虚拟机底层实现)可后期深入,避免一开始陷入细节
3. 理论 + 实践结合
  • 写代码复现 OOM、栈溢出、内存泄漏等问题
  • 用 JProfiler、VisualVM 等工具分析 GC 日志、内存快照
  • 分析生产环境的 GC 日志,优化 JVM 参数
4. 结合面试题学习
  • 刷 JVM 高频面试题,通过面试题反推知识点,加深理解
  • 总结面试题的答题思路,形成自己的知识体系
5. 读经典书籍
  • 精读《深入理解 Java 虚拟机》,反复阅读,每次都有新收获
  • 结合《Java 虚拟机规范》,理解 JVM 的底层原理
14.2 学习避坑指南
  • 不要只背理论,不实践:JVM 是实践性极强的知识,不实践等于没学
  • 不要一开始就啃底层细节:先掌握核心原理,再深入底层
  • 不要忽视 GC 日志分析:GC 日志是调优的核心,必须掌握
  • 不要盲目调优:调优前必须先监控、分析,再调整参数,避免盲目优化
14.3 学习路线图
  1. 基础阶段(1-2 周):掌握 JVM 体系结构、内存区域、类加载机制
  2. 核心阶段(2-3 周):深入垃圾回收算法、垃圾收集器、GC 日志分析
  3. 实战阶段(1-2 周):学习调优工具,解决 OOM、内存泄漏问题
  4. 进阶阶段(持续):理解 JVM 底层实现、即时编译、ZGC 等新特性

第二部分:JVM 体系结构与内存区域

P2 2、JVM 的体系结构

2.1 JVM 整体架构

JVM(Java Virtual Machine)是 Java 程序运行的基础,核心分为三大子系统 + 两大组件

复制代码
┌─────────────────────────────────────────────────┐
│                  JVM 体系结构                    │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐  │
│  │ 类加载子系统 │  │ 运行时数据区 │  │ 执行引擎  │  │
│  └─────────────┘  └─────────────┘  └──────────┘  │
│  ┌─────────────┐  ┌──────────────────────────┐   │
│  │ 本地方法接口 │  │ 本地方法栈(Native方法) │   │
│  └─────────────┘  └──────────────────────────┘   │
└─────────────────────────────────────────────────┘
2.2 核心组件详解
组件 核心作用
类加载子系统 负责加载.class文件,验证、准备、解析、初始化类,双亲委派机制核心
运行时数据区 JVM 内存管理核心,分为线程私有 (虚拟机栈、本地方法栈、程序计数器)和线程共享(堆、方法区)
执行引擎 执行字节码,包含解释器、即时编译器(JIT),HotSpot 虚拟机核心
本地方法接口 调用 Native 方法(如 C/C++ 实现的方法),连接 Java 与底层系统
本地方法栈 为 Native 方法服务,记录 Native 方法调用状态
2.3 JVM 核心特性
  • 跨平台:一次编写,到处运行(JVM 屏蔽了底层操作系统差异)
  • 自动内存管理:垃圾回收机制自动回收堆内存,避免手动内存管理的问题
  • 即时编译:JIT 编译器将热点代码编译为机器码,提升运行效率

P3 3、类加载器及双亲委派机制

3.1 类加载的生命周期

类加载分为5 个阶段

  1. 加载 :获取.class文件的二进制字节流,生成 Class 对象
  2. 验证:验证字节码的安全性、正确性,避免恶意代码
  3. 准备 :为类的静态变量分配内存,设置默认初始值(如int=0Object=null
  4. 解析:将符号引用替换为直接引用(如方法名替换为内存地址)
  5. 初始化:执行静态代码块、静态变量赋值,是类加载的最后一步

3.2 类加载器分类

JVM 提供 3 种核心类加载器,层级结构如下:

复制代码
启动类加载器(Bootstrap ClassLoader)
    ↳ 扩展类加载器(Extension ClassLoader)
        ↳ 应用程序类加载器(Application ClassLoader)
            ↳ 自定义类加载器(Custom ClassLoader)
  • 启动类加载器 :加载JAVA_HOME/jre/lib下的核心类库(如rt.jar),由 C++ 实现,无 Java 对象
  • 扩展类加载器 :加载JAVA_HOME/jre/lib/ext下的扩展类库
  • 应用程序类加载器 :加载用户项目classpath下的类,是默认的类加载器
  • 自定义类加载器 :继承ClassLoader,实现自定义加载逻辑(如加密类加载、热部署)

3.3 双亲委派机制

3.3.1 核心原理

当类加载器收到加载请求时,优先委托父类加载器加载,父类加载器无法加载时,才由自己加载。

复制代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查类是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 2. 委托父类加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 父类为null,委托启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载失败,不做处理
            }
            if (c == null) {
                // 4. 父类加载失败,自己加载
                long t1 = System.nanoTime();
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
3.3.2 核心优势
  • 避免类重复加载 :保证核心类库(如java.lang.Object)只加载一次
  • 保证核心类安全 :防止恶意代码替换核心类(如自定义java.lang.String
  • 实现类加载的层级管理:规范类加载流程,提升系统稳定性

3.4 打破双亲委派机制

  • 场景:热部署、SPI 机制(如 JDBC 驱动加载)、自定义类加载
  • 实现 :重写loadClass方法,不委托父类加载器,直接自己加载
  • 注意:打破双亲委派需谨慎,避免类安全问题

P4 4、Java 历史 - 沙箱安全机制

4.1 沙箱安全机制的诞生背景

Java 早期(Applet 时代)需要在浏览器中运行远程代码,为了防止恶意代码破坏本地系统,诞生了沙箱安全机制

4.2 沙箱安全机制核心原理

沙箱将 Java 程序限制在一个安全的运行环境中,限制程序对本地系统的访问权限,核心规则:

  • 限制文件读写:禁止程序读写本地文件
  • 限制网络访问:仅允许与代码来源服务器通信
  • 限制系统调用:禁止调用本地系统资源(如注册表、硬件)
  • 字节码验证:验证字节码的安全性,避免恶意代码
4.3 沙箱机制的演进
  • Java 1.0:严格沙箱,所有 Applet 代码都受限制
  • Java 1.1:引入签名类,信任签名的代码,放宽限制
  • Java 2+ :引入权限模型(SecurityManager),细粒度控制权限,沙箱机制演变为安全管理器
  • Java 17+SecurityManager被废弃,沙箱机制逐步退出历史舞台
4.4 沙箱机制的意义
  • 保障了 Java Applet 的安全运行,推动了 Java 的普及
  • 为 Java 安全体系奠定了基础,后续的安全机制均基于沙箱思想
  • 是 JVM 类加载验证、字节码验证等安全机制的源头

P5 5、Native、方法区

5.1 Native 方法详解
  • 定义 :Native 方法是由 ** 非 Java 语言(如 C/C++)** 实现的方法,用native关键字修饰,没有方法体
  • 作用:调用底层系统资源、提升性能、复用现有 C/C++ 代码
  • 示例Object.hashCode()Thread.start()Unsafe类的方法均为 Native 方法
  • 注意:Native 方法不受 JVM 内存管理限制,可能导致内存泄漏、系统崩溃
5.2 方法区(Method Area)
5.2.1 方法区核心定义

方法区是线程共享 的内存区域,用于存储类的结构信息,包括:

  • 类的全限定名、父类全限定名、接口
  • 类的构造方法、普通方法、静态方法
  • 类的静态变量、常量、运行时常量池
  • 类的字节码、即时编译后的机器码
5.2.2 方法区的演进
  • JDK 7 及之前:方法区在永久代(PermGen)中,属于堆内存的一部分,大小固定,易导致 OOM

  • JDK 8 及之后 :永久代被移除,方法区迁移到元空间(Metaspace),直接使用本地内存,大小动态调整

  • 核心区别

    特性 永久代(PermGen) 元空间(Metaspace)
    内存位置 堆内存 本地内存(直接内存)
    大小限制 固定大小,易 OOM 动态调整,默认无上限
    垃圾回收 回收效率低,易内存泄漏 自动回收,无 OOM 风险
    配置参数 -XX:PermSize-XX:MaxPermSize -XX:MetaspaceSize-XX:MaxMetaspaceSize
5.2.3 运行时常量池

运行时常量池是方法区的一部分,用于存储类的常量、符号引用、字面量,是 Class 文件常量池的运行时表示。

  • 特点 :动态性,运行时可将新的常量放入池中(如String.intern()
  • 区别:Class 文件常量池是静态的,运行时常量池是动态的

P6 6、深入理解一下栈

6.1 虚拟机栈(VM Stack)核心定义

虚拟机栈是线程私有的内存区域,每个线程创建时都会创建一个虚拟机栈,生命周期与线程一致。

  • 作用 :记录 Java 方法的调用状态,每个方法调用对应一个栈帧(Stack Frame)

  • 栈帧结构

    复制代码
    ┌─────────────────┐
    │  局部变量表      │  存储方法的局部变量、参数、this
    ├─────────────────┤
    │  操作数栈        │  存储运算的中间结果,是字节码执行的工作区
    ├─────────────────┤
    │  动态链接        │  指向运行时常量池的方法引用
    ├─────────────────┤
    │  方法返回地址    │  方法执行完后,返回调用者的地址
    ├─────────────────┤
    │  附加信息        │  异常表、调试信息等
    └─────────────────┘
6.2 栈的核心特性
  • 线程私有:每个线程的栈相互独立,无线程安全问题
  • 后进先出(LIFO):最后调用的方法最先执行完,栈帧入栈出栈遵循 LIFO
  • 栈大小限制 :栈大小由-Xss参数配置,栈过深会导致栈溢出(StackOverflowError)
  • 栈内存分配:栈内存是连续的,分配速度快,性能高
6.3 栈溢出与栈深度
  • 栈溢出场景:方法递归调用过深、方法调用链过长

  • 示例

    复制代码
    public class StackOverflowDemo {
        public static void recursive() {
            recursive(); // 无限递归,导致栈溢出
        }
        public static void main(String[] args) {
            recursive();
        }
    }
  • 解决方案 :优化递归逻辑、调整-Xss参数增大栈大小(不推荐,易导致内存浪费)

6.4 本地方法栈(Native Method Stack)
  • 作用:为 Native 方法服务,结构与虚拟机栈类似,记录 Native 方法的调用状态
  • 区别:虚拟机栈服务 Java 方法,本地方法栈服务 Native 方法
  • HotSpot 虚拟机:将本地方法栈与虚拟机栈合并,统一管理

P7 7、走近 HotSpot 和堆

7.1 HotSpot 虚拟机简介

HotSpot 是 Oracle/Sun 公司开发的主流 JVM 实现,是 Java 生态的核心虚拟机,具有以下特性:

  • 即时编译(JIT):将热点代码编译为机器码,提升运行效率
  • 自适应优化:根据程序运行情况,动态优化代码
  • 分代垃圾回收:将堆分为新生代、老年代,采用不同的垃圾回收算法
  • 开源:OpenJDK 开源,是 Java 虚拟机的标准实现
7.2 堆(Heap)核心定义

堆是线程共享 的内存区域,是 JVM 中最大的内存块,用于存储对象实例和数组,是垃圾回收的核心区域。

  • 核心特点
    • 线程共享,需考虑线程安全
    • 垃圾回收的主要区域(GC 堆)
    • 大小可通过-Xms(初始堆大小)、-Xmx(最大堆大小)配置
    • 物理上不连续,逻辑上连续
7.3 HotSpot 堆的内存布局

HotSpot 将堆分为新生代(Young Generation)老年代(Old Generation),比例默认 1:2:

复制代码
┌─────────────────────────────────────────────────┐
│                      堆内存                       │
│  ┌─────────────┐  ┌──────────────────────────┐   │
│  │  新生代      │  │  老年代                  │   │
│  │  (1/3堆大小) │  │  (2/3堆大小)            │   │
│  └─────────────┘  └──────────────────────────┘   │
└─────────────────────────────────────────────────┘
  • 新生代:存储新创建的对象,大部分对象朝生夕死
  • 老年代:存储存活时间长的对象,经过多次 GC 后晋升
7.4 堆内存溢出(OOM)
  • 场景:对象无限创建、内存泄漏、堆大小设置过小

  • 示例

    复制代码
    public class OOMDemo {
        public static void main(String[] args) {
            List<Object> list = new ArrayList<>();
            while (true) {
                list.add(new Object()); // 无限创建对象,导致OOM
            }
        }
    }
  • 错误信息java.lang.OutOfMemoryError: Java heap space


P8 8、新生区、永久区、堆内存调优

8.1 新生区(新生代)详解

新生代分为3 个区域,比例默认 8:1:1:

复制代码
┌─────────────────────────────────────────────────┐
│                    新生代                        │
│  ┌──────────┐  ┌──────┐  ┌──────┐              │
│  │  Eden区   │  │ S0区 │  │ S1区 │              │
│  │ (80%空间) │  │(10%) │  │(10%) │              │
│  └──────────┘  └──────┘  └──────┘              │
└─────────────────────────────────────────────────┘
  • Eden 区:存储新创建的对象,大部分对象在此区域
  • Survivor 区(S0/S1):存活对象的缓冲区,每次 GC 后存活对象复制到另一个 Survivor 区
  • 对象晋升规则:对象在 Survivor 区存活 15 次(默认)后,晋升到老年代;大对象直接进入老年代
8.2 永久区 / 元空间(方法区)调优
  • JDK 7 及之前(永久代)
    • 配置参数:-XX:PermSize=128m-XX:MaxPermSize=256m
    • 调优重点:避免永久代 OOM(java.lang.OutOfMemoryError: PermGen space
  • JDK 8 及之后(元空间)
    • 配置参数:-XX:MetaspaceSize=128m-XX:MaxMetaspaceSize=512m
    • 调优重点:限制元空间大小,避免占用过多本地内存
8.3 堆内存调优核心参数
参数 作用 推荐值
-Xms 初始堆大小 -Xmx设置为相同值,避免堆动态扩容
-Xmx 最大堆大小 物理内存的 1/4~1/2,服务器可设置为 8g~16g
-Xmn 新生代大小 堆大小的 1/3,如-Xms8g -Xmx8g -Xmn2g
-XX:SurvivorRatio Eden 区与 Survivor 区比例 默认 8,可调整为 6~8
-XX:MaxTenuringThreshold 对象晋升老年代的年龄 默认 15,可根据业务调整
-XX:+UseConcMarkSweepGC 使用 CMS 垃圾收集器(老年代) 适合低延迟应用
-XX:+UseG1GC 使用 G1 垃圾收集器 适合大堆内存(>4g)应用
8.4 堆内存调优最佳实践
  • 设置堆大小固定-Xms-Xmx相同,避免堆动态扩容导致的性能波动
  • 合理分配新生代大小:根据对象存活周期调整,避免对象频繁晋升老年代
  • 选择合适的垃圾收集器:低延迟应用用 G1/CMS,高吞吐量应用用 Parallel GC
  • 监控 GC 日志 :通过-XX:+PrintGCDetails-Xloggc:gc.log分析 GC 情况,优化参数

第三部分:GC 算法、JMM 与总结(对应 P10-P13 + JMM)

P10 10、GC 介绍之引用计数法

10.1 垃圾回收(GC)核心定义

GC(Garbage Collection)是 JVM 的自动内存管理机制,用于回收不再被引用的对象,释放内存,避免内存泄漏。

  • 核心目标:识别垃圾对象、回收垃圾对象、整理内存
  • 垃圾对象定义:不再被任何引用指向的对象
10.2 引用计数法(Reference Counting)
10.2.1 核心原理

为每个对象添加一个引用计数器,当有引用指向对象时,计数器 + 1;引用失效时,计数器 - 1;计数器为 0 时,对象为垃圾,可回收。

复制代码
对象A ← 引用1 → 计数器=1
对象A ← 引用2 → 计数器=2
引用1失效 → 计数器=1
引用2失效 → 计数器=0 → 回收对象A
10.2.2 优缺点
优点 缺点
实现简单,判定垃圾快 无法解决循环引用问题(如 A 引用 B,B 引用 A,计数器均为 1,无法回收)
实时性高,对象死亡立即回收 计数器维护开销大,影响性能
10.2.3 应用场景
  • Python、PHP 等语言使用引用计数法
  • JVM不使用引用计数法,因为无法解决循环引用问题

P11 11、GC 之复制算法

11.1 复制算法(Copying)核心原理

将内存分为两个相等的区域,每次只使用一个区域;当该区域满时,将存活对象复制到另一个区域,然后清空当前区域。

复制代码
┌─────────────┐  ┌─────────────┐
│  区域A      │  │  区域B      │
│  (使用中)   │  │  (空闲)     │
└─────────────┘  └─────────────┘
        ↓  复制存活对象
┌─────────────┐  ┌─────────────┐
│  区域A(清空) │  │  区域B(使用)│
└─────────────┘  └─────────────┘
11.2 复制算法的应用场景
  • 新生代:新生代对象朝生夕死,存活对象少,复制算法效率高
  • HotSpot 新生代实现:将新生代分为 Eden 区和两个 Survivor 区(S0/S1),比例 8:1:1,每次 GC 将 Eden 和一个 Survivor 的存活对象复制到另一个 Survivor 区
11.3 优缺点
优点 缺点
实现简单,回收效率高 内存利用率低,只能使用一半内存
无内存碎片 存活对象多时,复制开销大
11.4 优化:分代复制

HotSpot 将新生代分为 Eden 区和两个 Survivor 区,避免了内存浪费,同时保证了复制效率:

  • 大部分对象在 Eden 区死亡,每次 GC 仅复制少量存活对象到 Survivor 区
  • 两个 Surviv 区交替使用,保证内存无碎片

P12 12、GC 之标记压缩清除算法

12.1 标记 - 清除算法(Mark-Sweep)
12.1.1 核心原理

分为标记清除两个阶段:

  1. 标记:遍历所有对象,标记出存活对象(可达性分析)

  2. 清除:清除未标记的垃圾对象,释放内存

    ┌─────────────────────────────────┐
    │ 内存区域:存活 垃圾 存活 垃圾 存活 │
    │ ↓ 标记: √ × √ × √ │
    │ ↓ 清除: 存活 存活 存活 │
    └─────────────────────────────────┘

12.1.2 优缺点
优点 缺点
实现简单,无需移动对象 产生大量内存碎片,影响内存分配效率
适合存活对象多的场景 清除效率低,需遍历所有对象
12.2 标记 - 压缩算法(Mark-Compact)
12.2.1 核心原理

在标记 - 清除的基础上,增加压缩阶段:

  1. 标记:标记存活对象

  2. 压缩:将存活对象向内存一端移动,整理内存

  3. 清除:清除压缩后的垃圾对象

    ┌─────────────────────────────────┐
    │ 内存区域:存活 垃圾 存活 垃圾 存活 │
    │ ↓ 标记: √ × √ × √ │
    │ ↓ 压缩: 存活 存活 存活 │
    │ ↓ 清除: 清除后面的垃圾区域 │
    └─────────────────────────────────┘

12.2.2 优缺点
优点 缺点
无内存碎片,内存分配效率高 移动对象开销大,影响性能
适合存活对象多的场景(老年代) 标记、压缩、清除三个阶段,效率低
12.3 两种算法的应用场景
  • 标记 - 清除:适合老年代,存活对象多,垃圾对象少,内存碎片可接受
  • 标记 - 压缩:适合老年代,需要整理内存,避免内存碎片,如 Serial Old、Parallel Old 收集器
  • 复制算法:适合新生代,存活对象少,效率高

P13 13、GC 算法总结和鸡汤

13.1 GC 算法核心总结
算法 核心原理 适用场景 优点 缺点
引用计数法 计数器记录引用数,为 0 则回收 小对象、实时性要求高 实现简单、实时性高 无法解决循环引用、开销大
复制算法 分区域复制存活对象 新生代(存活对象少) 效率高、无碎片 内存利用率低、复制开销大
标记 - 清除 标记存活对象,清除垃圾 老年代(存活对象多) 实现简单、无需移动 内存碎片多、效率低
标记 - 压缩 标记 + 压缩 + 清除 老年代(需整理内存) 无碎片、分配效率高 移动开销大、效率低
13.2 分代回收思想

JVM 采用分代回收思想,根据对象的存活周期,将堆分为新生代和老年代,采用不同的 GC 算法:

  • 新生代 :对象朝生夕死,存活对象少,采用复制算法
  • 老年代 :对象存活时间长,存活对象多,采用标记 - 清除 / 标记 - 压缩算法
13.3 垃圾收集器总结

HotSpot 提供多种垃圾收集器,适配不同场景:

收集器 适用区域 算法 特点 适用场景
Serial 新生代 复制算法 单线程、简单 客户端应用、小内存
Parallel 新生代 复制算法 多线程、高吞吐量 服务器应用、高吞吐量
CMS 老年代 标记 - 清除 低延迟、并发 低延迟应用、互联网服务
G1 整堆 标记 - 压缩 分区域、低延迟 大堆内存(>4g)、低延迟
ZGC 整堆 染色指针 低延迟(<10ms) 超大堆、极致低延迟
13.4 学习 JVM 的 "鸡汤"
  • JVM 是 Java 的根基:掌握 JVM,才能真正理解 Java 的运行原理,写出高性能、稳定的代码
  • 理论 + 实践是唯一捷径:不要只背理论,要动手写代码、用工具分析,才能真正掌握
  • JVM 是面试的敲门砖:Java 后端面试必问 JVM,掌握 JVM,才能拿到心仪的 offer
  • 持续学习,不断深入:JVM 一直在演进(如 ZGC、Shenandoah),要持续学习,跟上技术发展

16. JMM(Java 内存模型)

16.1 定义

JMM(Java Memory Model,Java 内存模型)是一套抽象的逻辑模型,不是真实的硬件内存布局,它用来规范多线程环境下,线程如何与主内存交互、如何保证可见性、原子性、有序性。

16.2 核心作用
  • 屏蔽不同操作系统、硬件的内存访问差异
  • 解决多线程并发下的原子性、可见性、有序性问题
  • 规定线程工作内存与主内存的数据同步规则
16.3 三大特性(面试必考)
  1. 原子性 一个操作不可分割、不可中断,要么全部执行成功,要么全部不执行。保证手段:synchronizedLockAtomic 原子类。

  2. 可见性 一个线程修改了共享变量,其他线程能立即看到最新值。保证手段:volatilesynchronizedLock

  3. 有序性 程序执行顺序按照代码顺序执行,禁止指令重排。保证手段:volatile(禁止重排)、synchronizedhappens-before 原则。

16.4 volatile 关键字作用
  • 保证变量可见性
  • 禁止指令重排序
  • 不保证原子性
16.5 happens-before 原则

JMM 用来判断线程是否安全、数据是否竞争的核心规则,常见几条:

  • 程序次序规则
  • 锁定规则(synchronized 解锁一定发生于后续加锁之前)
  • volatile 变量规则
  • 线程启动规则
  • 传递性规则

17. JVM 整体总结

17.1 JVM 核心结构一句话总结

JVM 是运行在操作系统之上的虚拟计算机 ,通过类加载子系统 加载字节码,在运行时数据区 分配内存,由执行引擎 执行代码,并通过GC 自动回收垃圾,最终实现 Java 程序跨平台、安全、自动管理内存。

17.2 内存区域一句话总结
  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区(元空间)
  • 堆存对象,栈存方法调用,方法区存类信息
17.3 GC 一句话总结

根据对象生命周期分代回收

  • 新生代对象存活率低 → 复制算法
  • 老年代对象存活率高 → 标记 - 清除 / 标记 - 压缩
  • 目的是减少 STW、避免 OOM、提升程序稳定性
17.4 学习 JVM 的意义
  • 理解 Java 程序底层运行原理
  • 排查 OOM、内存泄漏、死锁、CPU 飙高问题
  • 服务端性能调优必备
  • 中高级 Java 面试核心考点

补充:三种 JVM(主流实现)

实现 特点 适用场景
HotSpot Oracle/Sun 官方实现,性能优异,生态最广 绝大多数 Java 应用
OpenJ9 IBM 开源,内存占用低,启动快 嵌入式、资源受限环境
GraalVM 新一代虚拟机,支持多语言,原生镜像 微服务、Serverless、多语言混合开发

补充:PC 寄存器(程序计数器)

  • 定义 :一块线程私有的小内存区域。
  • 作用 :记录当前线程正在执行的 Java 字节码的行号
  • 特性
    • 线程私有,无线程安全问题。
    • 唯一不会抛出 OOM 的区域。
    • 执行 Native 方法时,计数器值为undefined
相关推荐
coder阿龙2 小时前
基于SpringAI+Qdrant+Ollama本地模型和向量数据库开发问答和RAG检索
java·数据库·spring boot·ai·数据库开发
Gofarlic_OMS2 小时前
HyperWorks用户仿真行为分析与许可证资源分点配置
java·大数据·运维·服务器·人工智能
徒 花2 小时前
Python知识学习08
java·python·算法
chushiyunen2 小时前
milvus笔记、常用表结构
笔记·算法·milvus
Lyyaoo.2 小时前
【JAVA基础面经】== 和 equals() 的区别
java·开发语言·jvm
YunQuality2 小时前
六西格玛黑带三个月拿证经验分享
笔记·职场和发展·职场·学习方法
lifallen2 小时前
Flink Agent:RunnerContext 注入与装配演进分析
java·大数据·人工智能·语言模型·flink
爱丽_2 小时前
Tomcat 从 Socket 到 Servlet:机制主线、参数调优与线上排障(实战)
java·servlet·tomcat
QDYOKR1682 小时前
一文了解什么是OKR
大数据·人工智能·笔记·钉钉·企业微信