一、前言
JVM调优并非参数堆砌的艺术,而是一场基于数据与监控的精准手术。它旨在平衡内存、吞吐量与延迟,让Java应用在资源约束下发挥最大效能。
核心在于理解应用特性:是追求低延迟的金融交易,还是高吞吐的批处理?不同的目标,决定了完全不同的优化路径。
二、调优前的核心准备
在动手调整任何参数前,充分的准备是成功的一半。盲目调优往往适得其反。
2.1 明确性能目标
调优必须有明确目标导向,通常聚焦于三者之一: 低延迟:最小化垃圾回收(GC)导致的停顿时间(Stop-the-World),适用于实时交互系统。 高吞吐量:最大化应用在单位时间内的处理能力,适合离线计算、数据分析任务。 小内存占用:在有限内存资源下稳定运行,常见于容器化或资源受限环境。
2.2 收集与分析数据
没有度量,就没有优化。必须建立监控基线: 启用详细GC日志:使用
-Xlog:gc*或-XX:+PrintGCDetails参数记录每次GC行为。 利用监控工具:jstat -gc <pid> 1s实时观察;jmap分析堆内存分布;VisualVM、JProfiler进行深度剖析。 压力测试:在模拟真实负载下观察JVM表现,记录瓶颈。
三、JVM的内存模型

四、JAVA类加载的全过程是怎样的?什么是双亲委派机制?有什么作用?
- JAVA的类加载器
AppClassloader -> ExtClassloader -> BootStrap->Classloader每种类加载器都有他自己的加载目录。
- JAVA中的类加载器:
AppClassLoader ->ExtClassLoader -> URLClassLoader - >SecureClassLoader -> ClassLoader每个类加载器对他加载过的类,都是有一个缓存的。
- 双亲委派
向上委托查找,向下委托加载。 作用:保护JAVA的层的类不会被应用程序覆盖。
- 类加载过程
1. 加载:把Java的字节码数据加载到JVM内存当中,并映射成JVM认可的数据结构。2. 连接:分为三个小的阶段:2.1 验证:检查加载到的字节信息是否符合JVM规范。2.2 准备: 创建类或接口的静态变量,并赋初始值 半初始化状态2.3 解析:把符号引用转为直接引用3. 初始化:
- 一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
method{ ClassLoaderDemo1 c =new ClassLoaderDemo1(); c.xxx} GC1. 用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。2. JVM要实例化一个对象,首先要在堆当中先创建一个对象。-> 半初始化状态3. 对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。-> 多大年龄才会移入老年代? 年龄最大15, 超过一定年龄后,对象转入老年代。4. 当方法执行结束后,栈中的指针会先移除掉。5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
五、怎么确定一个对象到底是不是垃圾? 什么是GC Root?
1、引用计数: 这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。2、根可达算法: 这种方式是在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾。哪些是GC Root? Stack -> JVM Stack, Native Stack, class类, run-timeconstant pool 常量池, static reference 静态变量。
六、JVM有哪些垃圾回收算法?
- MarkSweep 标记清除算法
这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
- Copying 拷贝算法
为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存货对象的个数有关。
- MarkCompack 标记压缩算法
为了解决拷贝算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将端边界以外的所有内存直接清除。
七、JVM有哪些垃圾回收器?他们都是怎么工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?
7.1 STW
Stop-The-World。是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。
在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native 方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
7.2 JVM的垃圾回收器
Serial 串行。整体过程比较简单,就像踢足球一样,需要GC时,直接暂停,GC完了再继续。 这个垃圾回收器,是早期垃圾回收器,只有一个线程执行GC。在多CPU架构下,性能就会下降严重。只适用于几十兆的内存空间。Parallel 并行在串行基础上,增加多线程GC。PS+PO这种组合是JDK1.8默认的垃圾回收器。在多CPU的架构下,性能会比Serial高很多。CMS Concurrent Mark Sweep 核心思想,就是将STW打散,让一部分GC线程与用户线程并发执行。
7.3 整个GC过程分为四个阶段
1、初始标记阶段:STW 只标记出根对象直接引用的对象。2、并发标记:继续标记其他对象,与应用程序是并发执行。3、重新标记: STW 对并发执行阶段的对象进行重新标记。4、并发清除:并行。将产生的垃圾清除。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。
7.4 GC分为五个阶段
第一:初始标记 标记出GCRoot直接引用的对象。STW第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。第四:重新标记: 跟CMS中的重新标记过程是差不多的。第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
7.5 CMS的核心算法就是三色标记
三色标记:是一种逻辑上的抽象。将每个内存对象分成三种颜色:黑色:表示自己和成员变量都已经标记完毕。灰色:自己标记完了,但是成员变量还没有完全标记完。白色:自己未标记完。CMS通过增量标记 increment update 的方式来解决漏标的问题。
八、内存区域与核心参数调优
JVM内存是调优的主战场,合理分配是稳定性的基石。堆内存主要分为新生代(Young Generation)和老年代(Old Generation),元空间(Metaspace)则存储类元数据。
堆内存配置黄金法则
堆大小设置是调优第一步,遵循以下原则可避免常见陷阱:
1.固定堆大小,避免动态扩容
将初始堆(
-Xms)与最大堆(-Xmx)设为相同值,例如-Xms4g -Xmx4g。这能防止JVM运行时因扩容/收缩堆内存而产生的性能抖动。2. 合理分配新生代与老年代
新生代大小(
-Xmn)通常设置为堆的1/4到1/3。过小会导致频繁Minor GC,过大会延长Full GC时间。例如8G堆可设-Xmn2g。3. 关注元空间限制
对于动态生成类较多的应用(如使用Spring、Groovy),需设置元空间上限
-XX:MaxMetaspaceSize=512m,防止元空间无限膨胀导致内存溢出。
九、垃圾回收器选择与配置
垃圾回收器(GC)的选择是影响性能最关键的决定之一。JVM提供了多种GC实现,各有其适用场景。
| 回收器 | 核心目标 | 启用参数 | 适用场景 |
|---|---|---|---|
| G1 (Garbage-First) | 平衡吞吐与延迟,可预测停顿 | -XX:+UseG1GC | JDK9+默认,大内存、响应时间敏感型应用 |
| ZGC | 超低延迟(暂停<10ms) | -XX:+UseZGC | JDK11+,对延迟有极致要求的金融、交易系统 |
| Parallel GC | 高吞吐量 | -XX:+UseParallelGC | JDK8默认,后台计算、批处理任务 |
| Serial GC | 最小内存占用 | -XX:+UseSerialGC | 单核CPU或微服务、客户端应用 |
以最常用的G1回收器为例,关键调优参数包括:
- -XX:MaxGCPauseMillis=200:设置期望的最大GC停顿时间目标(毫秒),G1会尽力达成但不保证。
- -XX:InitiatingHeapOccupancyPercent=45:触发并发标记周期的堆占用率阈值,默认45%。
- -XX:G1NewSizePercent / -XX:G1MaxNewSizePercent:控制新生代占比范围(默认5%-60%)。
十、高级调优与场景化实践
掌握了基础配置后,需要针对特定场景和高级需求进行精细化调整。
🎯 场景化配置方案
- 低延迟场景(如金融交易)
核心是选用ZGC或Shenandoah,并限制堆内存软上限以防止内存占用过高影响响应。
java -XX:+UseZGC -Xms24g -Xmx24g -XX:ConcGCThreads=6 \ -XX:SoftMaxHeapSize=20g -jar app.jar
- 容器化环境(如Kubernetes)
必须启用容器支持,并基于内存限制按比例分配,避免容器因内存超限被杀死。
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 \ -XX:InitialRAMPercentage=50.0 -XX:+UseG1GC -jar app.jar
- 高吞吐量场景(如数据处理)
选用Parallel GC,并调整GC线程数以匹配CPU核心数,最大化利用计算资源。
java -XX:+UseParallelGC -XX:ParallelGCThreads=8 \ -Xms8g -Xmx8g -jar batch-app.jar
其他关键优化参数
- -XX:+AlwaysPreTouch:启动时预分配并触摸所有内存页,避免运行时因缺页中断产生延迟,但会延长启动时间。
- -XX:+UseStringDeduplication (G1):启用字符串去重,节省堆内存,尤其适用于大量重复字符串的应用。
- -XX:+ExplicitGCInvokesConcurrent:让
System.gc()触发并发GC,避免产生完全STW的Full GC。- -XX:+HeapDumpOnOutOfMemoryError:在内存溢出时自动生成堆转储文件,是定位内存泄漏的利器。
十一、常见问题与解决思路
调优过程中,总会遇到一些典型问题。以下是快速诊断与解决的思路。
| 问题现象 | 可能原因 | 排查与解决方向 |
|---|---|---|
| Full GC频繁 | 老年代空间不足、内存泄漏、对象过早晋升 | 检查对象生命周期;增大堆或老年代;调整-XX:MaxTenuringThreshold(晋升年龄);分析堆转储。 |
| GC停顿时间过长 | 大对象分配、Finalizer队列阻塞、高晋升率 | 避免创建大数组或大字符串;用Cleaner替代finalize();优化Survivor区比例。 |
| 内存占用居高不下 | 堆设置过大、缓存未释放、元空间膨胀 | 限制-XX:MaxMetaspaceSize;使用软引用缓存(如Caffeine);检查第三方库的内存占用。 |
| 应用吞吐量不达标 | GC线程占用过多CPU、锁竞争激烈、JIT编译开销大 | 调整-XX:ParallelGCThreads;优化同步代码块;考虑使用-XX:+TieredCompilation分层编译。 |
十二、调优心法:原则与误区
最后,记住这些原则能让你在调优路上少走弯路。
必须遵循的调优原则
先测量,后调优:永远基于监控数据(GC日志、性能指标)做决策,而非猜测。 一次只改一个变量:每次只调整一个参数并观察效果,否则无法定位变化原因。 回归测试:任何参数调整后,必须进行功能与性能回归测试,确保稳定性。 代码优化优先:JVM调优是“最后一公里”的优化。优先考虑减少对象创建、复用缓冲区、选择高效算法等代码级优化。
需要警惕的常见误区
盲目追求“最优”参数:不存在放之四海而皆准的最优配置,必须贴合应用实际负载。 参数堆砌:添加大量未经验证的“优化”参数,可能引入副作用或兼容性问题。 忽视操作系统与硬件:NUMA架构、SSD磁盘、网络带宽等底层资源同样影响JVM表现。 将性能问题简单归咎于JVM:如资深开发者所言,多数性能问题源于应用程序本身,而非JVM。完善的日志和监控是定位问题的前提。
调优的本质是理解与平衡
JVM调优不是魔法,而是一门基于监控数据、应用特性和资源约束的工程实践。它是在内存、CPU时间和响应延迟之间寻找那个微妙的平衡点。记住,没有“最好”的配置,只有“最适合”当前场景的配置。
本文详细介绍了JVM的内存模型、类加载过程、垃圾回收机制等内容。涵盖了从对象的创建到销毁的全过程,深入剖析了垃圾回收算法及其实现原理。

1万+

被折叠的 条评论
为什么被折叠?



