现象

实习生对新开发的推理接口进行压测时,发现tp99异常,大概在9000ms左右

acr1
acr1

young gc也十分频繁,还伴随着2次的full gc

acr2
acr2

cpu使用率也飙升到了100%

acr3
acr3

问题

通过jstask发现大量线程处于CaseFormat.convert方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
"JSF-BZ-22000-23-T-110" #247 daemon prio=5 os_prio=0 tid=0x00007fb0d8110800 nid=0x527 runnable [0x00007faf366ed000]
java.lang.Thread.State: RUNNABLE
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.google.common.base.CaseFormat.convert(CaseFormat.java:150)
at com.google.common.base.CaseFormat.to(CaseFormat.java:128)
at com.jd.jdl.sj.acr.utils.JDBCUtils.Populate(JDBCUtils.java:184)
at com.jd.jdl.sj.acr.mapper.RecommendedMapper.queryByAssociateData(RecommendedMapper.java:87)
at com.jd.jdl.sj.acr.algo.AcrAlgoStrategyA.predict(AcrAlgoStrategyA.java:73)
at com.jd.jdl.sj.acr.algo.AcrAlgoStrategyB.predict(AcrAlgoStrategyB.java:56)
at com.jd.jdl.sj.predict.service.impl.AlgoServiceImpl.predict(AlgoServiceImpl.java:26)
at com.jd.jdl.sj.predict.service.impl.AlgoServiceImpl$$FastClassBySpringCGLIB$$211fefd6.invoke(<generated>)

且jmap中发现jvm内存中存在大量String对象

acr4
acr4

最终定位到问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static List Populate(ResultSet rs, Class cc) throws SQLException, InstantiationException, IllegalAccessException {

try {
//结果集 中列的名称和类型的信息
ResultSetMetaData rsm = rs.getMetaData();
int colNumber = rsm.getColumnCount();
List res = new ArrayList();
Field[] fields = cc.getDeclaredFields();

//遍历每条记录
while (rs.next()) {
//实例化对象
Object obj = cc.newInstance();
//取出每一个字段进行赋值
for (int i = 1; i <= colNumber; i++) {
Object value = rs.getObject(i);
//匹配实体类中对应的属性
for (Field f : fields) {
// 驼峰转下划线命名
String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, f.getName());
if (name.equals(rsm.getColumnName(i))) {
boolean flag = f.isAccessible();
f.setAccessible(true);
f.set(obj, value);
f.setAccessible(flag);
break;
}
}
}
res.add(obj);
}
return res;
} catch (Exception e) {
throw new BusinessException();
}
}

这段代码是通过jdbc查询结果后,需要将ResultSet映射成pojo中的结果。
主要问题出在这行驼峰转下划线的代码中:

1
String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, f.getName());

查询一次数据库大概返回400行数据,每条数据25个字段,一次查询就会执行2.5w次这行代码,创建出2.5w个String。

解决方案

将驼峰转下划线前置,这样一次调用只会执行25次这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static List Populate(ResultSet rs, Class cc) throws SQLException, InstantiationException, IllegalAccessException {

try {

List res = new ArrayList();
Field[] fields = cc.getDeclaredFields();
Map<String, Field> fieldMap = new HashMap<>(fields.length);
for (Field field : fields) {
String fieldUnderscoreName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName());
fieldMap.put(fieldUnderscoreName, field);
}

//结果集 中列的名称和类型的信息
ResultSetMetaData rsm = rs.getMetaData();
//遍历每条记录
while (rs.next()) {
//实例化对象
Object obj = cc.newInstance();
//取出每一个字段进行赋值
for (int i = 1; i <= rsm.getColumnCount(); i++) {
Object value = rs.getObject(i);
String columnName = rsm.getColumnName(i);

Field field = fieldMap.get(columnName);
if (field == null) {
continue;
}
// boolean flag = field.isAccessible();
field.setAccessible(true);
field.set(obj, value);
// field.setAccessible(flag);
}
res.add(obj);
}
return res;
} catch (Exception e) {
throw new BusinessException();
}
}

压测结果性能大幅提升,tp99在20ms以下。

acr5
acr5