记一次GC调优
问题描述
最近在做一个项目,遇到了一些GC方面的问题,所以记录一下。
- 第一个问题:在大量数据的时候,需要new大量对象;
- 第二个问题:JVM参数的设置;
在我项目中就会遇到很多new大量对象的问题,以及JVM参数设置的问题。
下面我们来详细说说我的问题,我这边的具体问题是给用户推送相关文章,借助用户特征,给一些文章候选集算法排序,从而给每个用户推断出相对有兴趣的文章。平台有百万级用户,用户会有很多属性,文章候选集会有2000多篇;由于在排序的时候,会将每个用户的特征和文章的特征组织好给传给排序接口进行排序操作,所以在这个过程中就会遇到new大量对象;在排序的过程中也会new出大量的需要排序的向量data。
JVM参数介绍
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n :设置持久代大小 收集器设置
-XX:+UseSerialGC :设置串行收集器
-XX:+UseParallelGC :设置并行收集器
-XX:+UseParalledlOldGC :设置并行年老代收集器
-XX:+UseConcMarkSweepGC :设置并发收集器 垃圾回收统计信息
-XX:+PrintHeapAtGC GC的heap详情 -XX:+PrintGCDetails GC详情
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
-XX:+PrintTenuringDistribution 打印年龄信息等
-XX:+HandlePromotionFailure 老年代分配担保(true or false) 并行收集器设置
-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置
-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数
-XX: PretenureSizeThreshold 大对象直接进入老年区
JVM堆 详细介绍
- 新生代
- Eden Space
- From Survivor Space
- To Survivor Space
- 老年代
详细介绍
问题一:在大量数据的时候,需要new大量对象
由于new大量对象导致问题,Eden Space区迅速占满,然后会频繁年轻代GC,从而导致JVM大量的精力和时间都花在了GC上,于是就会影响程序运行效率;而我的程序对效率运行要求比较高,为300多W用户排序推断出10篇用户最感兴趣的文章,生成文件;需要在50分钟内完成从Mysql数据库中取出用户、候选集文章、然后通过排序将300多W用户数据写入文件。
解决办法:借助Object Pool的概念,不要new大量数据;比如一开始,我会去组装一个rank model对象,把用户特征、文章特征组装成一个对象,然后一共有2000多篇文章,每个用户每次需要new出2000多个文章对象,然后组装成一个用户的rank model对象。这里面300W*2000数据量大家可以算一下,会new出多少对象,这里我只是举出一个地方的例子;其实一开始实现功能是没有问题,但是就是效率慢,后面通过Object Pool的概念,发现我这里可以利用一些对象,比如文章候选集这个可以重复利用,直接传到rank对象中进行排序,这样我们就只要new2000多次;
后面还有就是在rank对象里面,一开始每个用户去排序的时候,都会new出一个需要排序的data对象来;一开始我们会把这些对象放入到一个List中;这样每个用户排序的时候都会组建一个List,然后刚开始的时候我们也没有对List进行初始化大小;这个list的大小其实是可以确定,就是我们需要排序的特征向量,文章特征+用户特征组装。第一次修改,将List固定好大小,这样就不用经常给List扩容,但是修改这个之后,还是没能很大的解决效率问题;
第二次,我将List改成了数组,然后所有用户共用一个数组,每次每个用户都向数组里面填充数据,这样,就会大大减小new对象,以及充分利用对象池的概念。最后这样基本解决了我的问题,效率提升到了1个小时左右。
问题二: JVM参数设置
上面也介绍我的第一个问题,由于第一个问题中会new大量对象,所以一开始我的JVM堆区的Eden Space很快就会占满;GC log如下:
[GC (Allocation Failure) [PSYoungGen: 2774574K->32144K(3424256K)] 3915335K->1172906K(5895680K), 0.0262079 secs] [Times: user=0.16 sys=0.01, real=0.03 secs]
要让log中打出GC日志,我们需要在Java程序运行的时候加上参数:-XX:+PrintGCDetails,这个在上面也具体介绍了JVM参数设置。
上面的log会频繁的打出,并且Eden Space 区基本在98%-100%的占用量之间。从而导致了频繁GC,并且Eden Space一直处于占满的状态,也就是new对象比GC释放还快,大大降低了效率。于是我通过修改-XX:NewSize=2000m来加大Eden Space区域,这样会导致我们的老年代区域变小。后面在修改完代码后,大大降低了new对象,但是我在之前调整了Eden Space,忘记修改回来,导致我的程序Eden Space空间比较大,老年代空间比较小,这时候在程序运行一段时间后,JVM log中会频繁出现Full GC,log 如下:
[Full GC (Ergonomics) [PSYoungGen: 18971648K->4339495K(20749824K)] [ParOldGen: 8683369K->8683028K(8683520K)] 27655017K->13022524K(29433344K), [Metaspace: 13816K->13816K(1062912K)], 31.9796242 secs] [Times: user=563.28 sys=0.66, real=31.98 secs]
这时候我突然想起来,我之前设置过Eden Space区域,调大Eden Space区域。由于老年代区域比较小,所以在程序运行一段事件后,导致老年代占满了,从而频繁的Full GC,这时候我们应该知道Full GC时间比较慢,一般在10s左右,所以我们应该让JVM尽量在年轻代就释放,而尽量减少Full GC;所以我调整了JVM参数,降低Eden Space区域,增大了年老代区域,于是确实跟我所想的一样,大大降低了Full GC的次数,也大大增加了我程序的运行效率。
经过最后的努力,程序效率确实提升到了50分钟左右。
书籍推荐
《深入理解Java虚拟机_JVM高级特性与最佳实践》