记一次JAVA 程序优化之旅
参与到一个项目中后,这个项目有一个非常棘手的问题,就是程序需要在初次启动时加载大量数据,高达80G,这会导致成本高筑,不得不使用裸金属服务器进行部署
上个版本现状
tps:1790 rt:12.94ms 80G
目标是 tps 不下降的情况下,尽量使内存占用下降
改进1
首次观察加载数据,发现这个数据具有一定的规律,都是部门或者场地等的代码,重复率较高,每个字符串出现次数平均80次;可以参考霍夫曼压缩算法的逻辑来处理,这里
于是通过构建数据字段生成Pair<String, Integer> 类似这样的数据结构,左边为字符串,右边为代表字符串的数字
当前我们项目字符串上限是60K个, 那么(只这个特例)只要用两个字节的数据就可以表达出这个字符串的Index(数字)
public class IndexArray {
private byte[] indexArray = new byte[0];
private final int UNIT;
public IndexArray(int size) {
if (size <= 255) {
UNIT = 1;
} else if (size <= 65535) {
UNIT = 2;
} else if (size <= 16777215) {
UNIT = 3;
} else {
UNIT = 4;
}
}
public void addIndex(int index) {
byte[] byteArray = new byte[UNIT];
for (int i = 0; i < UNIT; i++) {
byteArray[i] = (byte) ((index >> (i * 8)));
}
int srcBytesLength = this.indexArray.length;
byte[] mergedArray = new byte[srcBytesLength + byteArray.length];
System.arraycopy(this.indexArray, 0, mergedArray, 0, srcBytesLength);
System.arraycopy(byteArray, 0, mergedArray, srcBytesLength, byteArray.length);
this.indexArray = mergedArray;
}
}
这样通过两个字节就表达了至少40个字节的字符串。
通过这个思路,使内存从80G 下降到了 17G
但是遇到了新的挑战,就是tps 下降了,从tps:1700 下降到 1296
改进2
思考发现自己的寻址算法不是很好,是通过把Index的两个字节从字节数组中一个个取出,然后转换成Integer,再来比较的方式处理的,相对耗时;
这里改进为,将要用于比较的Index 转换为两个字节的数据,在IndexArray上按两个字节滑动比较,效率更好
优化后
tps:1352 rt:17.35ms
效率上升10%
改进3
在这个接口的算法逻辑中看到,这是个贪心算法,由于请求中的参数较多,同时又是反复取出比较,导致String 转 Index 这个过程的算法被反复调用,经检查这里也是比较耗时
于是将这个转化过程改进到进入核心算法之前,提前转换
优化后
tps: 1489 rt: 15.71ms
效率再上升10%
改进4
由于使用stopwatch打印关键步骤耗时过程中发现,在调用别的类或者本类中其他方法时,耗时不定但平均下来占比很大;怀疑是java[CMS(Concurrent Mark Sweep)]内存回收导致stop the world的原因导致的耗时,而系统为大内存,计算密集型程序;选型使用G1作为垃圾回收器进行实验
实验参数:
-server -Xmx85g -Xms85g -Xmn1g -XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=50
-XX:G1HeapRegionSize=16M
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=30
-XX:G1ReservePercent=15
-XX:G1RSetUpdatingPauseTimePercent=5
-XX:ParallelGCThreads=16
-XX:ConcGCThreads=8
(参数含义可以查文档获得)
效果比较显著
tps:3369 rt:6.72ms
达到了优化目标