JVM调优篇

增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生Full GC的时候,GC时间会相对比较长;如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?

#推荐配置


根据《Java Performance》里面的推荐公式来进行设置:

  • Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3~4倍,即Full GC之后的老年代内存占用的3~4倍。

  • 方法区(永久代,PermSize和MaxPermSize 或 元空间,MetaspaceSize和MaxMetaspaceSize)设置为老年代存活对象的1.2~1.5倍。

-XX:MetaspaceSize、-XX:MaxMetaspaceSize

关于这几个参数,发现某些实际场景与文档并不一致,建议阅读一下JVM参数MetaspaceSize的误解(opens new window)

  • 年轻代 Xmn 的设置为老年代存活对象的1~1.5倍。

  • 老年代的内存大小设置为老年代存活对象的2~3倍。

但是,上面的说法也不是绝对的,也就是说这只是一个参考值,根据多种调优之后得出的一个结论,大家可以根据这个值来设置一下我们的初始化内存,在保证程序正常运行的情况下,我们还要去查看GC的回收率、GC停顿耗时、内存里的实际数据来判断,Full GC是基本上不能有的,如果有就要做内存Dump分析,然后再去做一个合理的内存分配。

我们还要注意到一点就是,上面说的老年代存活对象怎么去判定?

#计算老年代存活对象


  • 查看日志(推荐):JVM参数中添加GC日志,GC日志中会记录每次Full GC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的Full GC之后的内存情况,根据多次的Full GC之后的老年代的平均空间大小数据来预估Full GC之后老年代的存活对象大小。即根据多次FullGC之后的内存大小取平均值

  • 强制触发Full GC(慎用):强制触发Full GC,会造成线上服务停顿(STW),要谨慎!建议的操作方式为,在强制Full GC前先把服务节点摘除,Full GC之后再将服务挂回可用节点,对外提供服务,在不同时间段触发Full GC, 根据多次Full GC之后的老年代内存情况来预估Full GC之后的老年代存活对象大小。

  • jmap -dump:live,format=b,file=heap.bin <pid> 将当前的存活对象dump到文件,此时会触发FullGC

  • jmap -histo:live <pid>打印每个class的实例数目,内存占用,类全名信息。.live子参数加上后,只统计活的对象数量. 此时会触发Full GC

  • 在性能测试环境,可以通过Java监控工具来触发Full GC,比如使用VisualVM和JConsole触发GC的按钮。

总结:查看日志的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发Full GC,所以日志中并没有记录Full GC的日志。在分析的时候就比较难处理。 所以,有时候也是需要强制触发一次Full GC,来观察Full GC之后的老年代存活对象大小。

#案例演示


通过IDEA启动SpringBoot工程,我们将内存初始化为1024M。我们这里就从1024M的内存开始分析我们的GC日志,根据我们上面的一些知识来进行一个合理的内存设置。

JVM设置如下:

-Xms1024M
-Xmx1024M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=64m
-Xss512K
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdump3.hprof
-XX:+PrintGCDateStamps
-Xloggc:log/gc-oom3.log

代码入口,主要是读取people表,该表只有一条数据:

/**
 * 性能优化案例3:合理配置堆内存
 */@RequestMapping("/getData")
publicList<People>getProduct(){
    List<People> peopleList = peopleSevice.getPeopleList();
    return peopleList;
}

项目启动,通过jmeter访问15000次(主要是看项目是否可以正常运行)之后,查看gc状态 :

YGC平均耗时: 0.07s * 1000/8 = 8.75ms,FGC未产生,吞吐量为1269.7。

看起来似乎不错,YGC触发的频率不高,FGC也没有产生,但这样的内存设置是否还可以继续优化呢?是不是有一些空间是浪费的呢?

为了快速看数据,我们使用了强制触发Full GC的方式,通过命令jmap -histo:live <pid> 产生几次Full GC,Full GC之后,使用的jmap -heap <pid> 或GUI工具来看的当前的堆内存情况,观察老年代存活对象大小如下:

如果jamp -heap报错,尝试使用jhsdb jmap --heap --pid <pid>, 或参考 彻底解决Jmap在mac版本无法使用的问题 (opens new window)

可以看到存活对象占用内存空间大概17.7M,老年代的内存占用为683M左右。 按照整个堆大小是老年代(Full GC)之后的3~4倍计算的话,设置堆内存情况如下:

Xmx=18*3=54M  至  18*4=72M 之间 

我们修改堆内存状态如下:

-Xms70M
-Xmx70M

修改后,再次进行压测,查看一下GC状态 :

调优之后,YGC平均耗时: 0.194s * 1000/111 = 17.47ms,FGC未产生,整体的GC耗时增加,吞吐量为1307.2。

虽然GC频率比之前的1024M时要多了不少,但是依然未产生Full GC,相对之前,节省了很大一块内存空间,吞吐量也没有很大影响,所以本次内存调整是比较合理的。依然手动触发Full GC,查看堆内存结构 :

#总结


内存相对紧张的情况下,可以按照上述的方式来进行内存的调优, 找到一个在GC频率和GC耗时上都可接受的一个内存设置, 可以用较小的内存满足当前的服务需要。 但当内存相对宽裕的时候,可以相对给服务多增加一点内存,可以减少GC的频率 ,GC的耗时相应会增加一些。 一般要求低延时的可以考虑多设置一点内存, 对延时要求不高的,可以按照上述方式设置较小内存。

如果在垃圾回收日志中观察到OutOfMemoryError,尝试把Java堆的大小扩大到物理内存的80%~90%。我们在进行调优的时候尤其需要注意的是堆空间导致的OutOfMemoryError以及一定要增加空间。 通常有以下方法:

  • 增加-Xms和-Xmx的值来解决老年代的OutOfMemoryError

  • 增加-XX:PermSize和-XX:MaxPermSize来解决permanent代引起的OutOfMemoryError(JDK 7之前);增加-XX:MetaspaceSize和-XX:MaxMetaspaceSize来解决Metaspace引起的OutOfMemoryError(JDK 8之后)

记住一点Java堆能够使用的容量受限于硬件以及是否使用64位的JVM。在扩大了Java堆的大小之后,再检查垃圾回收日志,直到没有OutOfMemoryError为止。如果应用运行在稳定状态下没有OutOfMemoryError就可以进入下一步了,计算活动对象的大小。

你会估算GC频率吗?

正常情况我们应该根据我们的系统来进行一个内存的估算,这个我们可以在测试环境进行测试,最开始可以将内存设置的大一些,比如4G这样,当然这也可以根据业务系统估算来的。

比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是(128 B / 1024 Kb / 1024M)* 1000 = 0.122M ,那么我们程序可能需要并发读取,比如每秒读取100次,那么(Eden区)内存占用就是0.122*100 = 12.2M ,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么(333M * 80%) / 12.2M = 21.84s ,也就是说我们的程序几乎每分钟进行两到三次YoungGC。这样可以让我们对系统有一个大致的估算。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值