当前位置: 首页 > news >正文

JVM Full GC 优化指南

频繁的 Full GC 会引发长时间的应用程序停顿(Stop-The-World),严重影响服务的响应速度和稳定性。为了帮助你系统地解决这个问题,我梳理了一个优化流程,并汇总了常见的 Full GC 触发原因及应对策略。

本文将注解介绍发生Full GC的常见的原因,并给出具体的排查方向,主要可以从代码层面上的优化以及参数上的调优。

具体定位问题的工具上怎么使用。

一 、概述

下面的流程图展示了核心的排查与优化步骤,你可以根据它来定位和解决问题:

JVM频繁Full GC
排查方向
内存泄漏排查
JVM参数调优
代码层面优化
生成堆转储文件
分析大对象/GC Roots
修复代码问题
调整堆与代大小
选择合适的GC器
调整元空间/晋升阈值
避免大对象
及时释放资源

二、排查内存泄漏

当发现 Full GC 在固定时间间隔频繁发生,并且老年代内存在每次 GC 后也未见明显回落,这通常预示着存在内存泄漏。你可以按照以下步骤排查:

2.1 生成堆转储文件:在发生 Full GC 后,使用 jmap -dump:format=b,file=heap.bin <pid> 命令导出堆内存快照。
2.2 分析堆转储文件

使用 Eclipse Memory Analyzer (MAT) 等工具分析导出的堆转储文件。重点关注:
1.重复创建的大对象
例如巨大的数组或集合。大对象因为年轻代空间较小而无法安置,JVM会把大对象直接放入到老年代,因此为了能够安置大对象,会更加频繁的触发Full GC压缩老年代的内存空间。这就对象的到来的危害,在不同的GC中会有不同的处理方式。但是本质上都是快速回收加上压缩空间,或者是在代码设计上避免大对象。

2.GC Roots 引用路径
查找哪些对象不该被引用却无法被回收,主要原因主要有以下几种:
1) 未正确释放的 ThreadLocal 变量。
原因分析:ThreadLocal 内部使用 ThreadLocalMap 存储数据,其 Key 是 ThreadLocal 对象的弱引用,Value 是实际存储的值(强引用)。
泄漏路径

  • ThreadLocal 对象被回收 → Key 变为 null
  • 但 Value 仍然被 Entry 强引用
  • 如果线程长时间运行(如线程池线程),Value 永远无法被回收

代码示例:

public class ThreadLocalMemoryLeak {// 线程池,线程会复用private static final ExecutorService executor = Executors.newFixedThreadPool(1);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {executor.execute(() -> {// 每次任务都创建新的ThreadLocal,但没有清理ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();threadLocal.set(new byte[1024 * 1024]); // 1MB数据// 模拟业务逻辑System.out.println("Task executed in thread: " + Thread.currentThread().getName());// 关键问题:没有调用 threadLocal.remove()// threadLocal 对象会被回收(弱引用),但value(1MB数据)会一直存在});Thread.sleep(100);}// 查看内存情况System.out.println("查看内存使用...");executor.shutdown();}
}

解决方案:

public class ThreadLocalFixed {private static final ExecutorService executor = Executors.newFixedThreadPool(1);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {executor.execute(() -> {ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();try {threadLocal.set(new byte[1024 * 1024]);System.out.println("Task executed");// 业务逻辑...} finally {// 必须在使用完后清理threadLocal.remove();}});Thread.sleep(100);}executor.shutdown();}
}

2)生命周期过长的缓存(如 ConcurrentHashMap)。
原因分析:使用 ConcurrentHashMap 等作为缓存时,如果没有合理的淘汰策略,对象会一直被引用,即使业务上已经不再需要。
代码案例:

public class CacheMemoryLeak {// 全局缓存,生命周期与应用相同private static final Map<String, Object> CACHE = new ConcurrentHashMap<>();private static final List<String> USED_KEYS = new ArrayList<>();public static void main(String[] args) throws InterruptedException {// 模拟不断向缓存添加数据for (int i = 0; i < 10000; i++) {String key = "data-" + i;byte[] data = new byte[1024 * 1024]; // 1MB数据CACHE.put(key, data);USED_KEYS.add(key);// 模拟业务逻辑:只使用前100个keyif (i >= 100) {// 但忘记从缓存中移除不再使用的数据// 导致后面的9900个1MB对象永远无法回收}if (i % 1000 == 0) {System.out.println("已添加 " + i + " 个对象到缓存");printMemory();}Thread.sleep(10);}}static void printMemory() {Runtime runtime = Runtime.getRuntime();long usedMemory = runtime.totalMemory() - runtime.freeMemory();System.out.printf("已使用内存: %.2f MB%n", usedMemory / (1024.0 * 1024.0));}
}

解决方案:

public class CacheMemoryFixed {// 使用弱引用或软引用缓存private static final Map<String, SoftReference<byte[]>> SOFT_CACHE = new ConcurrentHashMap<>();// 或者使用有大小限制的缓存(推荐)private static final Map<String, byte[]> LIMITED_CACHE = new LinkedHashMap<String, byte[]>(100, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {return size() > 100; // 限制缓存大小为100个元素}};// 最佳方案:使用专业的缓存框架private static final Cache<String, Object> GUAVA_CACHE = CacheBuilder.newBuilder().maximumSize(1000) // 最大容量.expireAfterAccess(10, TimeUnit.MINUTES) // 访问后10分钟过期.expireAfterWrite(1, TimeUnit.HOURS) // 写入后1小时过期.build();public static void addToCache(String key, byte[] data) {// 方案1:软引用缓存(内存不足时自动回收)SOFT_CACHE.put(key, new SoftReference<>(data));// 方案2:固定大小缓存synchronized (LIMITED_CACHE) {LIMITED_CACHE.put(key, data);}// 方案3:Guava缓存(推荐)GUAVA_CACHE.put(key, data);}
}

3)匿名内部类持有外部类引用导致无法回收。
原因分析:匿名内部类隐式持有外部类的引用,如果这个内部类被长生命周期对象引用,会导致外部类也无法被回收。

代码案例:

public class AnonymousClassMemoryLeak {private String largeData = new String(new byte[1024 * 1024]); // 1MB数据public interface EventListener {void onEvent(String event);}private EventListener listener;public void registerListener() {// 匿名内部类隐式持有外部类 AnonymousClassMemoryLeak.this 的引用listener = new EventListener() {@Override`        public void onEvent(String event) {// 这里可以访问外部类的largeData字段System.out.println("Event: " + event + ", data: " + largeData);}};// 假设listener被全局事件管理器持有GlobalEventManager.register(listener);}public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {AnonymousClassMemoryLeak leak = new AnonymousClassMemoryLeak();leak.registerListener();// 虽然leak对象超出作用域,但由于listener被全局持有// 导致leak对象(包含1MB数据)无法被GC回收if (i % 10 == 0) {System.gc();Thread.sleep(100);printMemory();}}}static void printMemory() {Runtime runtime = Runtime.getRuntime();long usedMemory = runtime.totalMemory() - runtime.freeMemory();System.out.printf("已使用内存: %.2f MB%n", usedMemory / (1024.0 * 1024.0));}
}// 模拟全局事件管理器
class GlobalEventManager {private static final List<EventListener> LISTENERS = new ArrayList<>();public static void register(EventListener listener) {LISTENERS.add(listener);}
}

解决方案:

public class AnonymousClassFixed {private String largeData = new String(new byte[1024 * 1024]);public interface EventListener {void onEvent(String event);}private EventListener listener;public void registerListener() {// 方案1:使用静态内部类,不持有外部类引用listener = new StaticEventListener(largeData);// 方案2:使用弱引用持有外部类WeakReference<AnonymousClassFixed> weakThis = new WeakReference<>(this);listener = new EventListener() {@Overridepublic void onEvent(String event) {AnonymousClassFixed outer = weakThis.get();if (outer != null) {System.out.println("Event: " + event + ", data: " + outer.largeData);}}};GlobalEventManager.register(listener);}// 静态内部类,不持有外部类引用private static class StaticEventListener implements EventListener {private final String data;StaticEventListener(String data) {this.data = data; // 只持有需要的数据,而不是整个外部类}@Overridepublic void onEvent(String event) {System.out.println("Event: " + event + ", data: " + data);}}// 提供清理方法public void unregisterListener() {if (listener != null) {GlobalEventManager.unregister(listener);listener = null;}}
}

2.3 代码修复

根据分析结果,修复代码中的问题,例如及时清理无用的对象引用、确保资源被正确关闭、修复可能导致循环引用的逻辑等。

注意:在内存泄漏问题没有解决之前,盲目调整JVM参数通常效果不佳。切记在工作过程中主要以预防为主、治理为辅。

三、 调整 JVM 参数

如果排除了明显的内存泄漏,或者 Full GC 的发生伴随着 对象晋升失败(promotion failed)、**并发模式失败(concurrent mode failure)**或 元空间(Metaspace)不足等现象,那么 JVM 参数可能需要进行调整。

  1. 合理分配堆与各代大小

    • 初始堆与最大堆:建议将 -Xms-Xmx 设置为相同值,避免堆内存动态调整带来的性能损耗。
    • 新生代大小:使用 -Xmn 参数显式设置新生代大小。过小的新生代会导致大量对象提前进入老年代,容易引发 Full GC。通常可以尝试设置为整个堆大小的 1/3 到 1/2
    • Survivor 区比例:通过 -XX:SurvivorRatio 调整 Eden 区与 Survivor 区的比例。过小的 Survivor 区 可能容纳不下 Minor GC 后的存活对象,导致它们直接进入老年代。
  2. 选择合适的垃圾收集器
    根据你的应用需求和硬件配置,选择合适的垃圾收集器,并调整相应参数,可以显著减少GC停顿。以下是一些主流的选择:

    收集器适用场景与参数建议
    G1适用于大内存、追求低停顿的应用。可设置 -XX:MaxGCPauseMillis=200(最大停顿时间目标)和 -XX:InitiatingHeapOccupancyPercent=45(触发并发周期的堆占用率)。
    CMSJDK 9 之前常用,需警惕晋升失败和并发模式失败。可尝试调低 -XX:CMSInitiatingOccupancyFraction(如从70调至75),并搭配 -XX:+UseCMSCompactAtFullCollection(Full GC时开启碎片整理)。
    ZGCJDK 11+ 引入,适用于超大堆和超低延迟(暂停时间<10ms)场景。
  3. 留意其他参数

    • 元空间(Metaspace):JDK 8 之后,元空间溢出也会触发 Full GC。可以通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 设置合适的大小,例如 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
    • 大对象阈值:通过 -XX:PretenureSizeThreshold 控制直接进入老年代的对象大小,避免短命大对象侵占老年代。
    • 禁用显式GC:使用 -XX:+DisableExplicitGC 禁止在代码中调用 System.gc(),因为这会建议JVM进行Full GC。

4 大对象的处理
核心的配置思路是:让大对象直接进入老年代,并优化老年代的垃圾回收,以避免因大对象在新生代来回拷贝造成的性能损耗,或过早触发Full GC。

下表汇总了关键的JVM参数:

参数名作用与说明推荐场景与值
-XX:PretenureSizeThreshold对象超过此值直接进入老年代。避免在Eden和Survivor区间大量内存复制。例如 -XX:PretenureSizeThreshold=1M注意:仅对SerialParNew新生代收集器有效。
-XX:MaxTenuringThreshold设置对象晋升老年代的年龄阈值。降低此值可让存活一定次数的对象提前进入老年代。默认15。可适当调低,如 -XX:MaxTenuringThreshold=8
-Xmn显式设置新生代大小。增大新生代可降低Minor GC频率,给对象更多在新生代回收的机会。通常为堆大小的1/3到1/2。G1收集器不要设置此参数。
-XX:SurvivorRatio调整Eden区与Survivor区的比例。增大Eden可延缓Minor GC。例如 -XX:SurvivorRatio=8 (Eden:Survivor=8:1)。
-XX:+UseG1GC启用G1垃圾收集器。G1设有Humongous区专门处理大对象(通常超过Region大小的50%)。堆内存较大(如8G以上)或关注延迟稳定的应用。
-XX:G1HeapRegionSize在G1中,此参数决定Region大小,也间接决定何种对象被视为大对象(Humongous对象)。手动指定,如 -XX:G1HeapRegionSize=8M

5 不同收集器的配置策略

  • ParNew + CMS 组合
    如果你的应用使用此组合,可以这样配置:

    -XX:+UseParNewGC
    -XX:+UseConcMarkSweepGC
    -XX:PretenureSizeThreshold=1M  # 让1MB以上对象直接进老年代
    -Xmn2g  # 设置新生代大小,例如堆大小为6G时
    -XX:SurvivorRatio=8
    -XX:CMSInitiatingOccupancyFraction=70  # 老年代使用70%时触发CMS
    

    要点是使用PretenureSizeThreshold避开新生代,并通过-Xmn设定合适的新生代。

  • G1 收集器
    对于G1,你通常不需要直接配置大对象阈值,G1会自动在Humongous区分配大对象。

    -XX:+UseG1GC
    -XX:G1HeapRegionSize=8m  # 若堆内存8G以内且有较多大对象推荐设置此值
    -XX:MaxGCPauseMillis=200  # 设定目标暂停时间
    

    G1的调优重点在G1HeapRegionSize(影响大对象判定)和暂停时间目标。

四、 优化应用代码

良好的编码习惯是从根源上减少 Full GC 的关键。

  • 避免创建过大的对象:如超大的数组或集合。尽量分块处理数据。
  • 谨慎使用大对象:如果无法避免创建大对象,应确保这些对象是长时间使用的,以避免它们迅速占满老年代空间。
  • 及时释放资源:对于如文件句柄、数据库连接等资源,确保使用后及时关闭。
  • 优化常用类:例如,避免在循环或频繁调用的方法中创建 SimpleDateFormat 等重量级对象,可以考虑使用 ThreadLocal 进行缓存。

五、 常用监控与诊断工具

  • 命令行工具
    • jps:查看 Java 进程 ID。
    • jstat -gcutil <pid> <interval>:实时监控 GC 状态,包括各代使用率和 GC 次数/时间。
    • jstack <pid>:查看线程堆栈,可用于分析死锁、线程阻塞等问题。
  • 图形化工具
    • JVisualVM:JDK 自带,可监控内存、线程、CPU,并支持堆转储和分析。
    • Eclipse MAT:强大的堆转储分析工具,擅长定位内存泄漏。
    • Arthas:阿里开源的线上诊断神器,功能强大。

希望这份指南能帮助你定位并解决 Full GC 的烦恼。如果你的应用部署在特定的环境(例如 Docker 容器),或者有特殊的性能指标(如要求亚秒级延迟),可以提供更多信息,我们可以进一步探讨更具体的优化方案。

http://www.dtcms.com/a/562153.html

相关文章:

  • 如何在百度网站收录提交入口快速开发平台破解版
  • Linux系统编程——进程退出及状态回收
  • j动态加载网站开发wordpress域名如何申请
  • 响应式设计网站给别人做网站会连累自己吗
  • 赣榆网站建设xxiaoseo广西美丽乡村建设网站
  • 3、电机控制——VF控制学习总结
  • 多表分页联查——EF Core方式和Dapper方式
  • 做网站要找什么公司信息门户网站制作费用
  • wordpress网站后缀网站开发文档网站
  • 营销单页网站模板网站建设 6万贵不贵
  • 体育西网站开发设计深圳市住房和建设局官网
  • 数据结构 10 二叉树作业
  • 网站建设视频教程 百度云如何做网站赚钱6
  • HTML5 测验
  • 沧州网站建设王宝祥wordpress能恢复修改前吗
  • 有没有专门做京东天猫的人才网站e4a能建设网站吗
  • Java记录类:简化数据载体的新选择
  • 郑州做网站开发销售潍坊做网站
  • C++—string(1):string类的学习与使用
  • 做一张网站专栏背景图网页设计模板网站
  • 关于企业网站建设的市场比质比价调查报告手机制作ppt的软件免费
  • 做外贸网站可以收付款吗电商网站建站
  • 响水专业做网站手机wap网站怎么做
  • 催收网站开发要看网的域名是多少
  • 怎么用ps做网站幻灯片做一个app的成本
  • 河南省台前县建设局网站织梦小说网站源码
  • 在线免费视频网站推广安卓小程序开发入门
  • 网站维护电话站长统计代码
  • 数学分析简明教程——2.3 (未完)
  • 计网5.3.3 TCP连接管理