JVM的默认参数是经过了大神的验证的,是可以支持一般大多数的场景的,所以没必要下不要轻易改动。如果是代码的问题,改代码就好,不要为了练手强行更改参数。
常见的JVM问题表现上可分为:CPU飙高和内存飙高。
对于CPU飙高的原因可能是频繁的GC或者是出现锁竞争。
对于内存飙升的原因可能是内存泄漏或者是gc速度低于对象创建速度。
这是个公司ERP系统(卡券相关业务),某次运营人员在导出订单时反馈一直导不出来。查看日志发现出现OOM的异常。由于该服务设置了 -XX:+HeapDumpOnOutOfMemoryError 的JVM参数,所以当出现OOM时会有堆的dump文件。
找运维拿了dump日志后,放入VisualVM查看,找到占用堆内存比例大的对象为订单对象,通过引用链找到上层的引用对象为list集合,找到list对应代码的位置,发现该list是在导出订单接口中使用到。当用户点击导出时,根据条件查询出所有满足的订单数据集合,然后遍历list写入excel。
为了验证推理,询问运营的操作过程来复现场景。点击了下发现日志并没报OOM,看导出的按钮并没做点击后的置灰,所以又点击了几次后发现日志出现OOM了。询问运营核实后的确运营在点击后短时间内没看到excel文件所以又点击了几次。
因为上个月有比较大的节假日,公司还顺便做了些营销活动,所以上个月京东券之类的购物券卖得好,产生的订单量比平日的多了5倍,所以导出的数据量大了,并且转化为excel需要的对象以及写入excel也是个耗时的过程。
首先设计上就不是很合理,前端按钮没置灰,后端没校验重复操作。
设计上改进如下:
更改完设计、代码后线上导出在线查看heap情况,基本基本没影响,每次list的大小并没达到大对象,list没出现逃逸,所以list还在年轻代。
由于更多的是设计、代码上的问题,所以该次JVM问题并没调整JVM参数。
该服务主要为消费kafka的异步服务,在测试环境运行正常,在生产环境就出现频繁的gc,相同的jvm配置。
查看年轻代gc次数(接近1min/次)、单次耗时、内存变化 报表,老年代gc次数、单次耗时、内存变化 报表,发现主要是年轻代gc的问题,年轻代gc虽然单次耗时低于1秒,每次回收效率高,但是发生的次数多。
猜测可能是因为测试环境kafka消息较少,生产环境kafka消息较多,创建对象速度快,并且执行中产生的大部分对象都是临时性的对象, 所以导致年轻代发生频繁gc。
由于仅仅是内存太小的问题,并且不存在内存泄漏,所以直接扩大机器内存使整个年轻代的内存变大就好了。调整后经过一段时间观察,发现年轻代gc频率变低了。整个服务的性能提高了,吞吐量上去了。
在上面扩大物理内存后,虽然年轻代gc频率降低了,但是FGC的频率高,并且出现OOM问题。
查看老年代的gc频率和内存变化情况报表,发现内存变大快,回收时效率低,猜测存在内存泄漏。
找运维拿了GC日志,对堆dump文件进行分析,查看发现占据内存最大的对象所在的代码位置,发现原来是一个用于写入数据库的对象。由于项目中需要向clickhouse写入数据,clickhouse对写入QPS有限制,太高时会出现报错,所以这里采用批量写入,为了批量写入用集合来存储数据,达到一定量或者达到指定时间就批量写入。由于原先设置累计数量达到5千条写入一次,超过5分钟没写入就写入一次。由于设置的时间过长导致对象在内存中保留太久,并且对象是可达的,导致无法回收,对象都进入了老年代,进而出现OOM。
将时间有原来的5分钟改为1分钟,将累计个数由5千改为100。观察一段时间后发现不再出现OOM。
下面总结一些出现JVM问题时的处理策略:
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:`-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾。
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
//survivor区和Eden区大小比率
指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
-XX:InitialTenuringThreshol=7
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发GC会增加老年代GC的频率。
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM
原因: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。
注意: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。
XX:MaxDirectMemorySize