点击上方 蓝字 关注我们
Click blue above to follow US
我们有一个java系统,需要从固定的文件目录中读取csv格式的文件,然后将文件内容读取后在进行一系列的转换以及数据库操作,包含文件内容转换为java对象、内容去重、查询oracle、生成目标数据集合、数据存入oracle和mongodb。
起初,文件目录中有四个文件,一个4.3G,另外三个都是几百兆,四个文件一起有5.3G,大约1200万条初始数据。
程序之前使用的文件都比较少,因此之前设置的主要jvm参数是`-Xms64m -Xmx64G -XX:NewRatio=1 -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/`,之前运行时也都很正常,但是这一次却几个小时都不结束。
出现上边的问题,程序日志中却没有抛出异常,一直处在停顿状态。
由于缺乏这方面的经验,便只能常规的使用`top`以及`free`命令查看机器的内存以及cpu等状态,然后就发现在运行过程中当前进程的cpu占用持续保持400%-600%,很显然是不正常的。
看到这些数据,似乎就是因为cpu忙不过来,第一感觉可能是stream的问题,这套代码大量的使用了`stream`以及`parallelStream`,而`parallelStream`底层的`ForkJoin`实际是多线程处理。
于是使用`cat /proc/cpuinfo| grep “cpu cores”| uniq`查看了下机器的cpu信息,发现测试机是单核的cpu。
既然如此,那么是否真的就是因为单核导致的多线程不断切换,从而使cpu卡住呢?于是我把里边的`parallelStream`全部改成了`stream`。
但是在这个过程中,却意外的在使用`df -hl`命令查看磁盘空间的时候,发现/opt/磁盘使用量飙升,一直到100%。
进一步跟踪后,发现在这个目录下产生了一个java_pid42156.hprof的文件,正是这个达到了几十G的文件耗尽了磁盘空间,而这个文件就是在程序内存溢出的时候记录相关信息的文件。
有了上边的新发现后,也就有了新的预测,猜测cpu持续300%-600%的现象很可能是因为内存溢出。
但是心中却依旧不太理解,仅仅5.3G的文件,Xmx都设置64G了,还能内存溢出?
疑问留在心中不能马上寻找答案,因为其他人等着测试结果,于是只能先简单粗暴的再jvm参数中增加内存,把Xmx64G改为了Xmx80G。
同时,考虑到内存不够时扩容问题,也一并把Xms改成了20G。
网上很多地方建议Xmx和Xms设置成一样的,但是考虑到这个程序每次运行后就会关闭,而每次的文件大小也都不一样,因此就没有设置为一样,避免小文件时也占用太大的内存。
通过这样的修改后,终于运行成功,最终得到了可以交付的数据。
但是呢,从开始运行到得到最终结果,这个程序却运行了6H15min,这是在是一个难以让人接受的时间。
更加诡异的是,在进行另一个目录的测试时,发现2.1G的文件却仅仅只需要不到8分钟。
5.3G和2.1G,数据量比例不到1:2.5,但是处理时间的比例确是1:46.8。
因此,尽管程序运行得到了最终结果,但是这个处理时间肯定是有问题的。
一时间,没有太好的思路,代码中也没有觉得可以优化的地方,抱着试一试的态度,再次增加内存,把Xmx加到了95G,再次运行的时候发现时间一下子从6H15min降到了1H40min。
性能一下子提升了四倍,看起来好像还可以吧,然而并不是。
这机器的总内存也就128G,真的能够就一下子给这个程序95G吗?
既然不行,那就只能进一步分析,于是搬出了更多的工具,像`top -Hp`、`printf`、`jstack`,例如我某次运行的进程id是117325:
使用`top -Hp 117325`找到了占用cpu资源最高的子进程id,例如117345;
然后使用`printf %x 117345`把这个id转换为16进制1ca61;
然后使用`jstat 117325 |grep -A 200 1ca61 `
上边的输出结果中,看到有大量的`GC Thread`相关内容,于是进一步使用命令`jstat -gcutil 117325 1000 30`查看gc情况。
这不看不知道,一看吓一跳,在已经运行的50分钟里,gc时间就占了38分钟。
再进一步细看,YGC仅仅发生了十几次后就不再发生,也仅仅占用了50秒,而FGC则发生了三百多次,消耗了两千多秒,这怎么看都是不合理的。
不明白是什么原因导致发生了这么多的FGC,但是通过了解相关技术点,我大概了知道了什么情况下会触发FGC,用一个不太准确的说法,就是jvm中老生代不够用了。(FGC有一些配置会影响FGC的触发,所以不能单纯的说老生代,例如阈值)
如果有了解jvm参数的,应该会注意到我开头提到的一个设置,即`-XX:NewRatio=1`,这个参数实际就是设置的老生代和新生代的比例,很显然这里是设置的1:1。
我不太清楚之前为什么会被设置为1,但是我知道如果我希望增大老生代,那么我可以增大这个值,于是我就改成了`-XX:NewRatio=2`。
让人欣慰的是,在Xmx依旧是95G时,程序再次运行,终于从1H40min变成了26min。
在不考虑内存合不合适的情况下,起码这个时间是可以接受的了。
时间可以接受了,但是内存是肯定不能接受的,于是只能进一步优化,先尝试着把`Xmx95G`改成了`Xmx80G`,`NewRatio`依旧是2。
于是我进行了简单的计算,算到在95G内存时、`-XX:NewRatio=2`的情况下,老生代应该是63G,那么能否让最大80G的时候老生代也达到63G呢?
既然是可以配的,理论上当然可以,于是想到就做,再次通过简单计算后,我把`-XX:NewRatio=2`改成了`-XX:NewRatio=4`,这种情况下老生代最大应该是64G。
让人有点小兴奋的是,当做了上边的修改后,再次运行程序,一下子从5H55min变成了30min。
在兴奋的同时,我似乎找到了一点点规律,这样看起来,是否是保证老生代63G左右就可以了呢?
于是我进一步减少Xmx的配置,同时修改`-XX:NewRatio`,然后就发现,当`Xmx75G`、`-XX:NewRatio=5`时,也只需要29min,这时候算下来的老生代最大是62.5G。
我终于松了一口气,也进一步确定了那个想法,只要我让最大老生代能达到63G左右,应该就可以了。
然而,我又被无情的打脸了,当我把Xmx降到70G,NewRatio也设置成9的时候,得到的却是`java.lang.OutOfMemoryError: Java heap space`。
当我把Xmx继续加到72G的时候,依旧是`java.lang.OutOfMemoryError: Java heap space`。
我的脸色,瞬间难看到了几点,因为我们的目标,是64G,,甚至更低。
似乎,从jvm参数层面已经到了再也无法优化的地步,怎么办?
可是代码层面,那是最最开始就想要下手的地方,正是因为感觉无法下手,然后才有了之后的一系列jvm参数的优化。
怎么办?于是我又用上了其他的工具,像`jProfiler`、`jvisualvm`。
为了测试,我在本地把文件缩小到了770m,然后在工具中会看到类似下图的数据:
可以明显的看到整个过程中内存使用一直呈现上升趋势,期间时不时的下降,一直到持续占用8G左右的内存。
但是尽管一直在发生着gc,程序占用的内存依旧是在不断的上升着。
能想到的情况无非两种,要么是有什么大对象一直存在着不能被销毁,要么就是产生了很多不能被回收的对象。
到底是什么对象或者说是哪些对象呢?于是我在jvisualvm中打开了heapdump,然后就看到了类似下图的内容:
之所以说是类似,是因为公司电脑的截图我没法拿来用,本地并没有模拟一模一样的数据,结构也是相似,所以实际大小也有区别。
-
实际`char[]`是2972483224byte,约等于2.77G;
-
`String`是2792366808byte,约等于2.6G;
-
等同于`CTest`的是2391236928byte,约等于2.22G;
-
还有`Double`是398539560byte,约等于0.37G;
-
`Integer`的120736400byte,约等于0.11G。
List<CTest> ll=getFiles1().stream().map(FileTest::createObject1)
.flatMap(List::stream).distinct().collect(Collectors.toList());
实际上一直存活的最终对象就是这个List<CTest>,直接相关的CTest对象一共2.22G。
但是分析CTest类的定义会发现,里边有12个String属性,有1个Double属性,还有2个Integer属性。
这样看起来,似乎都对上了,但是这时候我头脑短路,始终想不明白为什么占用最多的是`char[]`,这个类中没有定义`char[]`啊,甚至我一度陷入误区的认为是在io流处理的过程中生成了`char[]`对象,且莫名其妙的没有释放。
就这样焦头烂额好一阵子后,突然像是顿悟般,才想起来我竟然忽略了String的底层就是`char[]`,如果这样看,似乎全都说的通的,所有占用大量内存的类都能找到归属了。
如果按这个比例粗略的计算,770m源文件最终出来的是8G的java对象,那么5.3G就是56G。
这样看来,似乎目前的程序便只能是`Xmx75G `、`NewRatio=5`这样了,是真的只能这样了吗?一时间又没有了头绪。
同时,脑海中也有了新的疑问:`String`和`String`底层的`char[]`竟然会同时占着各自的内存?难道不是String仅仅指向数组的引用吗?有点颠覆了认知。
原文始发于微信公众号(java全栈基地):记一次java程序从6H到30min的优化过程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/39793.html