最近在项目组测试有开发一个导表检查的需求,大致是在策划提交表更新到svn之后,svn发送post-commit到策划表数据服务,而后策划表数据服务下载excel文件,更新数据之后,将导表检查下游服务所需的检查数据进行整合,然后post到导表检查服务。在这个过程中,由于做的仓促没有考虑设计,最后策划表数据服务的JVM发生了OOM问题。经过一番排查改进了设计,解决了这个问题。
JVM的内存结构跟GC流程是老生常谈的话题。Java8去掉了永久区的设定,更改为了Metaspace存储类的元信息、常量等内容,而剩下来依然是新区eden和老区old。当eden区空间满的时候触发young gc,将eden区根节点不可达的对象清除,存活的对象转到survivor区。survivor区的对象如果经历了某一个数(可配置,最高15)的young gc后仍然存活,就会转到old区。如果短时间内创建大量对象,且eden区放不下,young gc没有清除过多数据的情况下,多余的数据会转到old区。如果old区也放不下,就出现OOM(Out Of Memory)。一般业务层面导致OOM的原因基本上是一次产生大量对象,或者是内存泄露,没有及时清除不需要的数据引用。
策划表数据的业务逻辑上是下载数据->更新数据->进行导表检查的一个流程,其中下载、更新数据这一环节会产生大量的对象,而同样进行导表检查时为了发导表检查所需的数据,也会产生大对象——需要将数据序列化为json字符串。在这个流程里自己犯了几个错误导致OOM的问题,具体如下:
- 导表检查的设计直接采用request数据然后在response里获取导表检查结果的方式,并没有增加回调接口,使得如果导表检查方出现逻辑问题不能及时返回结果的话,发送的导表数据json以及序列化后的结果将不能被gc,导致内存泄漏。因此增加了一个回调接口供导表检查下游调用,使得httpclient对象能够释放,从而request的json数据可以被young gc给消除掉。
- request接口采用第三方的库,传参是json string,但是在实际post时候会调用getBytes,无形中增加了内存开销。因此直接把第三方库代码扣下来,并且直接序列化json为bytes,减少内存开销。
- 导表检查前会导入数据,导入数据会不断下载excel文件并提取其中内容,这个步骤有触发young gc的可能。如果某次导入数据之后需要导表检查,就手动申请一次gc(),避免潜在的问题。
- 导表检查所在的容器规格升级为8G内存,并在jvm启动选项中设定eden区跟老区各一半,survivor区最大总共占1/4的eden区。
最终暂时解决了这个问题,如果后续有其它的优化策略,还要继续补充。
更新:后续找到了内存泄露的原因,是导表过程中缓存大量数据所致,最终修复了这个缺陷。至于JVM方面,一般情况下,基本上也不需要调参的啦~