【八股消消乐】项目中如何优化JVM内存分配?

发布于:2025-05-11 ⋅ 阅读:(20) ⋅ 点赞:(0)

在这里插入图片描述

😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!

题目

💬技术栈:JVM

🔍简历内容:基于GCViewer工具分析日志,优化JVM内存分配解决频繁GC问题,实现万级抢购接口QPS提高了近60%,响应时间降低了近50%。

🚩面试问:
(1)如何查看 JVM 堆内存分配;
(2)对象在堆中的生命周期;
(3)优化JVM内存分配的手段有哪些?;
(4)使用GCViewer分析日志过程中,指标的含义;


在这里插入图片描述

💡建议暂停思考10s,你有答案了嘛?如果你有不同题解,欢迎评论区留言、打卡。


答案

什么时候进行JVM内存分配:频繁的GC【导致上下文切换等性能问题,从而降低系统的吞吐量、增加系统的响应时间】且是正常的对象创建和回收。

对象在堆中的生存周期

(1)当我们新建一个对象时,对象会被优先分配到新生代的 Eden 区中,这时虚拟机会给对象定义一个对象年龄计数器(通过参数 -XX:MaxTenuringThreshold 设置)。
(2)当 Eden 空间不足时,虚拟机将会执行一个新生代的垃圾回收(Minor GC)。这时 JVM 会把存活的对象转移到 Survivor 中,并给对象的年龄 +1。对象在 Survivor 中同样也会经历 MinorGC,每经过一次 MinorGC,对象的年龄将会 +1
(3)内存空间也是有设置阈值的,可以通过参数 -XX:PetenureSizeThreshold 设置直接被分配到老年代的最大对象,这时如果分配的对象超过了设置的阀值,对象就会直接被分配到老年代,这样做的好处就是可以减少新生代的垃圾回收

查看 JVM 堆内存分配

java -XX:+PrintFlagsFinal -version | grep HeapSize 
jmap -heap 17284

在这里插入图片描述
这台机器上启动的 JVM 默认最大堆内存为 1953MB,初始化大小为 124MB。

JDK1.7 中,默认情况下年轻代和老年代的比例是 1:2,我们可以通过–XX:NewRatio 重置该配置项。年轻代中的 Eden 和 To Survivor、From Survivor 的比例是 8:1:1,我们可以通过 -XX:SurvivorRatio 重置该配置项

JDK1.7 中如果开启了 -XX:+UseAdaptiveSizePolicy 配置项,JVM 将会动态调整 Java 堆中各个区域的大小以及进入老年代的年龄–XX:NewRatio 和 -XX:SurvivorRatio 将会失效,而 JDK1.8 是默认开启 -XX:+UseAdaptiveSizePolicy 配置项的。

JDK1.8 中,不要随便关闭 UseAdaptiveSizePolicy 配置项,除非你已经对初始化堆内存 / 最大堆内存、年轻代 / 老年代以及 Eden 区 /Survivor 区有非常明确的规划了。否则 JVM 将会分配最小堆内存,年轻代和老年代按照默认比例 1:2 进行分配,年轻代中的 Eden 和 Survivor 则按照默认比例 8:2 进行分配。这个内存分配未必是应用服务的最佳配置,因此可能会给应用服务带来严重的性能问题。

JVM 内存分配调优

模拟一个抢购接口,假设需要满足一个 5W 的并发请求,且每次请求会产生 20KB 对象,我们可以通过千级并发创建一个 1MB 对象的接口来模拟万级并发请求产生大量对象的场景。

@RequestMapping(value = "/test1")
public String test1(HttpServletRequest request) {
	List<Byte[]> temp = new ArrayList<Byte[]>();
	
	Byte[] b = new Byte[1024*1024];
	temp.add(b);
	
	return "success";
}

AB测试结果:

在这里插入图片描述

当并发数量到了一定值时,吞吐量就上不去了,响应时间也迅速增加。

分析GC日志:

通过设置 VM 配置参数,将运行期间的 GC 日志 dump 下来:

-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/log/heapTest.log

-XX:PrintGCTimeStamps:打印 GC 具体时间;
-XX:PrintGCDetails :打印出 GC 详细日志;
-Xloggc: path:GC 日志生成路径。

收集日志后,使用 GCViewer 工具打开:

常见指标:
GC 频率:高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来说好了许多,但过多的 MinorGC 仍会给系统带来压力
堆内存:堆内存分为年轻代内存和老年代内存。首先我们要分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。如果内存不足或分配不均匀,会增加 FullGC,严重的将导致 CPU 持续爆满,影响系统性能。
吞吐量:频繁的 FullGC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降
延时:JVM 的 GC 持续时间也会影响到每次请求的响应时间

在这里插入图片描述

结果:

  • 主页面显示 FullGC 发生了 13 次
  • 右下角显示年轻代和老年代的内存使用率几乎达到了 100%

FullGC 会导致 stop-the-world 的发生,从而严重影响到应用服务的性能,所以需要调整堆内存的大小来减少 FullGC 的发生

调优:

(1)调整堆内存空间减少 FullGC:通过日志分析,堆内存基本被用完了,而且存在大量 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间。

java -jar -Xms4g -Xmx4g heapTest-0.0.1-SNAPSHOT.jar

-Xms:堆初始大小;
-Xmx:堆最大值。

测试结果:吞吐量提高了 40% 左右,响应时间也降低了将近 50%。

在这里插入图片描述
查看 GC 日志,发现 FullGC 频率降低了,老年代的使用率只有 16% 了:

在这里插入图片描述

(2)调整年轻代减少 MinorGC:将年轻代设置得大一些,从而减少一些 MinorGC

java -jar -Xms4g -Xmx4g -Xmn3g heapTest-0.0.1-SNAPSHOT.jar

测试结果:

在这里插入图片描述
查看 GC 日志,发现 MinorGC 也明显降低了,GC 花费的总时间也减少了

在这里插入图片描述
(3)设置 Eden、Survivor 区比例:在 JVM 中,如果开启 AdaptiveSizePolicy,则每次 GC 后都会重新计算 Eden、From Survivor 和 To Survivor 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。这个时候 SurvivorRatio 默认设置的比例会失效

我们可以通过 -XX:-UseAdaptiveSizePolicy 关闭该项配置,或显示运行 -XX:SurvivorRatio=8 将 Eden、Survivor 的比例设置为 8:2。大部分新对象都是在 Eden 区创建的,我们可以固定 Eden 区的占用比例,来调优 JVM 的内存分配性能。

再次测试:吞吐量提升了,响应时间降低了。

在这里插入图片描述
在这里插入图片描述

📌 [ 笔者 ]   文艺倾年
📃 [ 更新 ]   2025.5.10
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到