前言

当平常谈论到JVM调优的时候,很多人都喜欢开玩笑的说:“JVM需要调优,那肯定是你钱没冲够,加大内存就好了”。玩笑归玩笑,但是真的如玩笑一样,加大内存就可以了吗?加大内存真的会带来性能提升吗?

高并发场景下的内存调优

我们什么地方需要内存调优,肯定是高并发的情况下;以互联网的秒杀场景来说,用户每次的请求到来,系统首先需要检查库存是否充足,然后校验用户余额是否足够支付该商品,如果足够那么就扣除用户余额和商品库存。我们假设每个订单处理过程会产生0.2MB大小空间的内存,每秒产生100M的内存对象,每个订单处理时间为1秒,那么这100M对象在1秒后将变为垃圾对象。

未调优场景下的情况

假设我们生产虚拟机的内存为3G,未做任何调优情况下,运行时数据区内存分配结构如下:

我们都知道对象分配,首先分配在eden区。当eden区无法分配内存时,会触发YGC清理新生代,并把存活下来的对象复制到survivor区。如果survivor区域的对象经历了多次垃圾回收依然存活,会直接晋升到老年代。同时如果survivor相同年龄所有对象的大小,超过survivor区的一般,这些对象直接进入老年代。

有了这些理论基础,我们来想象一下此时JVM的运行情况:

对象首先分配在新生代,新生代是800M。也就是说每8秒新生代内存耗尽,需要进行YGC。进行YGC并不是说这800M的对象都是垃圾,都需要回收。可能还存在大量对象不是垃圾,我们就假设有100M的对象不是垃圾。

进行YGC的时候回收了800M的垃圾回收了700M,看起来回收效率好像挺高,但是我们还有100M的存活对象需要解决。你可能回收“好办呀,不是丢到survivor区吗?”。

但是事实并没有那么简单,仔细观察我们会发现survivor区的容量是100M但是我们需要复制的对象也是100M。你此时可能紧张了起来“哦,那么根据动态年龄判断,所有相同年龄对象的容量如果超过survivor区的一般,这些对象就直接晋升到老年代”。嗯,确实此时这些对象会存放到老年代。

事情好像稍微得到了解决,我们来预估一下此次垃圾回收的效率。eden区域是800M,每1秒产生100M的对象,那么就是说每8秒进行一个YGC。也就是说每8秒会有100M的对象进入老年代,老年代是2G,相当于每2分钟我们的old区域就会满会触发Full GC。Full GC是一种非常耗时的垃圾回收,一般来说老年代采用标记整理算法,毕竟我们使用次算法是因为老年代的对象大部分都是存活对象。但是我们现在老年代的对象大部分都是垃圾。不但会有大量内存碎片,而且效率也特别低。

那么好玩的就来了,我们正在做一个大促活动,但是我们的系统每2分钟就要爆卡一下,体验就会非常不好。所以这里就是我们就想办法减少Full GC

解决方案一:钱没冲够,加大堆空间

很多人都喜欢看玩笑的说:“JVM需要调优,那肯定是你钱没冲够,内存翻一倍你看还需要调优吗?”。这样的人我建议腾讯“优先录取”。但是玩笑归玩笑,大部分时候,我们硬怼服务器好像确实可以解决办法,而且这也确实是一种方法。毕竟钱能解决的问题都不是问题!

根据上述理论来说,我们增大堆空间的内存,直接新生代增加到2400M。看似能解决问题,但是我们的YGC再进行垃圾回收的时候经历了扫描和复制。过大的新生代会使得扫描垃圾的时候特别耗时,同时新生代里面需要复制的对象也会加大。

结论就是如果无脑的加大内存,我们可能减少了Full GC,但是结果就是YGC的时间越来越长了。这也就是面对越来愈大的堆,传统的垃圾收集器显得力不从心,所以才有了后来的ZGC,当然这里并不是这一节我们讨论的。

解决方案二:加大新生代,减少老年代

高并发场景,如果代码没有任何问题,那么建议加大新生代,同时老年代够用就好。YGC采用的是复制算法,如果你只是单纯的加大总空间,Eden没有增加多大,那么首先会导致新生代扫描时间加大,其次如果高并发请求的时候,1m内大量请求到达,快速堆满了新生代,此时就会触发YGC。但是触发YGC的时候根据可达性分析算法,此时Eden里面大量对象又是存活的,此时需要将大量这些对象复制到老年代。

回想我们新生代为什么要采用复制算法,无非是新生代大都是朝生夕死的,大量对象在一次YGC的时候就已经死亡,只需要复制少量的对象,成本很低。但是这样反而本末倒置。

此时还是3G的内存,我们做如下的调整,结果会怎么样呢?

我们加大了eden区域,可以减少触发新生代YGC的频率,同时也增大了survivor区域,可以让更多的对象撑过1次垃圾回收,避免直接进入老年代。同时也避免因为老年代填满而不得不触发Full GC的情况。

其他解决方案(具体看情况)

减小虚拟机栈

每次方法调用都会为我们创建一个栈帧存放局部变量表/操作数栈等信息,默认情况下是1M。虽然1M看起来不大,但是高并发情况下也会占用大量内存。如果情况允许,可以稍微减少一下虚拟机栈的默认内存大小。

增大垃圾收集线程数

如果你使用的是并行垃圾回收器,可以适当的增大垃圾收集线程数

查看堆区的信息

jmap -heap [进程号]

查看堆空间被哪些对象给占用了

jmap -histo [进程号] | head -20

统计GC的情况

jstat -gc [pid] [统计间隔-毫秒数] [统计几次]

jstat -gc 4030 5000 20 | awk ‘{print 14,16,$17}’

作者:活在梦里丶

来源:blog.csdn.net/qq_25448409/article/details/108558011