第一部分: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
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐