最近在做一个图形渲染工具的时候,程序跑着跑着就卡住,几秒甚至十几秒没反应。一开始以为是图像算法太耗资源,后来发现其实是 JVM 在频繁做 Full GC。每次一触发,整个应用就像被按了暂停键,用户体验特别差。这种情况其实不少见,尤其在处理大图或批量导出时更容易暴露出来。
先看现象:GC 日志告诉你真相
打开 GC 日志是最直接的办法。加上这几个参数启动 Java 程序:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
运行一段时间后查看日志,如果看到 Full GC (System) 或 Full GC 频繁出现,尤其是间隔只有几十秒,那问题就比较明显了。比如我那天看到日志里每 30 秒一次 Full GC,堆内存从 2G 慢慢涨到顶,然后被清掉一半,反复循环。
常见原因:别急着调参数,先查代码
很多人第一反应是调 JVM 参数,比如加大堆内存。但治标不治本。真正该做的是检查代码有没有“偷偷”吃内存的地方。图形处理中常见的坑包括:
- 加载高清图时用了
BufferedImage,但没及时释放; - 缓存了大量图片对象,比如用 HashMap 存了几百个
RenderedImage实例; - 用了第三方库做图像合成,底层可能创建了临时像素数组,没及时回收。
有个项目里,我们为了提升速度,把用户最近打开的 10 张大图全缓存在内存里。结果一张 4K 图占 50MB,10 张就是 500MB,再加上其他对象,Young 区很快就满,对象提前晋升到老年代,老年代撑满后就只能 Full GC。
优化方向:从小处改起
先把缓存策略改了,只保留最近 3 张图,其余写入磁盘缓存。同时在图像处理完后手动置空引用:
bufferedImage = null;
虽然 Java 有自动回收,但明确释放能让 GC 更快识别垃圾对象。另外,把大图处理拆成小块,避免一次性申请大内存。比如用 ImageReader 的区域读取功能,分片加载而不是整张载入。
JVM 参数调整:辅助手段
代码改完后,再配合合适的 GC 策略。如果是 JDK 8,推荐用 CMS(虽然已弃用,但在旧项目还常见):
-XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
如果是 JDK 11 及以上,直接上 G1:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
设置一个合理的最大停顿目标,让 G1 自动调节 Region 回收节奏,避免一口气扫完整个老年代。
监控不能少
上线前用 jstat -gc 命令看看实际运行时的 GC 频率。也可以接上 JVisualVM,实时观察堆内存变化。如果老年代增长缓慢且 Full GC 几天才一次,基本就稳了。
解决 Full GC 频繁的问题,关键不是背参数,而是理解内存是怎么被用掉的。尤其是在图形类应用中,数据量大、生命周期复杂,更得精打细算。