java程序6H到14min及80G内存到28G的优化过程(补充记录)

前言

前边有发过一篇代码优化的文章:记一次java程序从6H到30min的优化过程

大概描述的是一个java程序从最大内存至少80G运行6H30min,到最大内存降到75G并且运行30min的优化过程。

但是实际上这个结果并不达标,基于现有的环境,期望的最大内存不能超过64G,并且在可以接收的处理时间内这个内存应该越小越好。

终于又经过了几天的奋战后,这个结果变成了28G和14min。(这里的时间,并非指单纯读文件的时间,而是整个程序所有业务的完整时间)

最终优化方案解析

达到上述效果的原因,主要是两个方面,一是文件的拆分读取,二是数据结构的变更。

文件拆分

在这个代码的业务中,每次需要读取四个文件的内容进行处理。

需要经过基础文件内容的读取、源数据去重、后续各种分组、各种过滤、各种业务去重、各种关联业务数据查询、各种关联业务数据生成、多表入库数据生成、多表和多数据源数据入库等一系列的操作。

经过反复分析,这四个文件虽然在一定阶段的时候需要合并,但是实际上并不是一开始就需要同时读取以及合并到一起。

因此就把之前使用流式读取并最终放到一个List中的逻辑,修改为了分开读取,并且各自存到不同的List中。

与此同时,每个文件读取之后会立即进行后续可以单独处理的逻辑,处理完成之后就销毁这个源数据的List,使得读下一个文件的时候,上一个List就已经可以被回收。

但是,经过这样的修改之后,发现并没有太大变化。

又进一步分析后,发现之前的内存溢出直接发生在读源文件以及源数据的过滤阶段。

而现在虽然已经销毁了一部分源数据的List内容,但是由于经过了后续的逻辑处理,使得内存中又有了新的业务数据,只是换了方式而已。

又由于在5.3G的文件中,实际是3个几百兆,一个4.3G,于是原本文件列表中4.3G文件排在最后读取的时候,其他三个文件处理后的数据已经占用了很多内存,就导致整个程序依旧需要很大的内存才能运行通过。

于是我又做了一个小小的修改,对读取的文件列表进行了一个排序,先读最大的这个文件。

这样修改之后,发现最大内存可以从75G降低到70G了,总的运行时间还是30min左右。

然而,这个结果依然是不能满足要求。

数据结构的变更

在分开了文件读取依旧不能满足要求的情况下,进一步分析代码以及文件的数据,发现这些文件有一个规律,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
11111,1,AAAAA,10,,,111.22,WW,XX
11111,1,AAAAA,20,,,10.22,WW,XX
11111,1,AAAAA,30,,,1.22,WW,XX
11111,1,AAAAA,40,,,21.22,WW,XX
11111,1,AAAAA,50,,,111.22,WW,XX
11111,2,AAAAA,10,,,31.22,WW,XX
11111,2,AAAAA,20,,,111.22,WW,XX
11111,2,AAAAA,30,,,51.22,WW,XX
11111,2,AAAAA,40,,,181.22,WW,XX
11111,2,AAAAA,50,,,81.22,WW,XX
2222,1,AAAAA,10,,,111.22,WW,XX
2222,1,AAAAA,20,,,10.22,WW,XX
2222,1,AAAAA,30,,,1.22,WW,XX
2222,1,AAAAA,40,,,21.22,WW,XX
2222,1,AAAAA,50,,,111.22,WW,XX
2222,2,AAAAA,10,,,31.22,WW,XX
2222,2,AAAAA,20,,,111.22,WW,XX
2222,2,AAAAA,30,,,51.22,WW,XX
2222,2,AAAAA,40,,,181.22,WW,XX
2222,2,AAAAA,50,,,81.22,WW,XX

很显然,这里边有大量的重复内容。

之前程序中定义的数据存储对象中,属性实际就是和上边每一行一一对应的,那么这里有20行,则最终的List中就有20个对象,这20个对象中有大量重复的内容。

实际上,一开始测试的时候就已经知道这个,也感觉到修改结构的话应该能提升很大的效率,但是由于后续逻辑太复杂,所以一直不愿意从这层面下手。

直到似乎用光了所有容易的手段还是不达标后,就不得不硬着头皮从这里下手。

最终的数据结构修改为了重复的内容只出现一次,而变化的内容改为List存储。

也就是说之前的对象属性都是普通类型,而现在是一部分普通类型加一部分的List类型。

之前的一个对象是这样:

1
2
3
4
5
6
7
8
9
10
11
12
{
"a": "11111",
"b": 1
"c": null,
"d": "AAAAA",
"e": 10,
"f": null,
"g": null,
"h": 111.22,
"i": "WW",
"j": "XX"
}

上边举例的内容就会有20个这样的对象,那么修改后则是变成了下边这样:

1
2
3
4
5
6
7
8
9
10
11
12
{
"a": "11111",
"b": [1,1,1,1,1],
"c": null,
"d": "AAAAA",
"e": [10,20,30,40,50]
"f": null,
"g": null,
"h": [111.22,10.22,1.22,21.22,111.22]
"i": "WW",
"j": "XX"
}

这样的结构下,同样是上边举例的数据,最终的List中实际就只有4个对象,大大的减少了内存中重复存储的数据。

实际上,单纯的这种数据结构变更是很简单的,难的是,后续各种逻辑都是基于上边的数据结构进行处理,所以这种数据结构的变更导致了后续一系列的逻辑修改。

关于性能优化的一些感想

原本64G就应该达标,结果意外的降到了30G,任务应该是可以暂时交付了。

这个过程中有一些关于性能优化的感悟,也一并在这里做一个记录。

世上没有万能的药

当我把之前那一篇文章分享到朋友圈的时候,很感谢有几个朋友提出了各种看法和建议。

有一些让我有了新的灵感和思路,有的其实并不适用于当前的场景,但是不管哪一种,都让我进一步感受到世上没有万能的药。

例如,有朋友说JDK8默认的垃圾回收器是G1,就是最好的,不需要优化,很显然这是有问题的。

首先,在这个优化的过程中,我有看过默认的gc,并不是G1,同时我去网上查了,默认gc是G1的应该是jdk9。

其次,既然jdk提供了这么多的垃圾回收器以及各种参数,自然是有应用场景的,怎么可能说默认的就不需要优化了呢,最多是通常情况下使用默认的就够了。

再例如,有朋友说文件读取之后对象的内存大小和文件大小应该是多少的比例,这个也是有问题的。

很显然,文件的大小和读取文件存储的对象大小究竟应该是什么比例,需要看这个对象的数据结构是怎么定义的,就像我同样的文件,使用两种不同的数据结构存储,结果就决然不同。

心态很重要

上边其实也提到了,一开始就知道文件有很多重复的内容,如果改变了数据结构,大概率能够提升效率。

但是鉴于业务逻辑的复杂,所以一开始就是不愿意从这方面下手,总是想从简单的地方解决,也总是想着是不是提升一些就行了。

事实证明,其实真正咬牙去啃硬骨头的时候,远没有想象中的那么困难。

推荐文章