数据库全盘扫描导致的内存溢出

情景

同步 10万的数据到内容库,总是在执行 10多分钟之后突然出现 OutOfMemoryError。

1
Exception in thread "pool-1-thread-38" java.lang.OutOfMemoryError: Java heap space# 

探索

之前一直以为是线程池的原因,所以调整了线程池的大小

1
2
3
4
5
6
7
8
9
10
executorService = new ThreadPoolExecutor(20, 50,
60l, TimeUnit.MINUTES, new ArrayBlockingQueue(1000), (r, executor) -> {
if (!executor.isShutdown()) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});

调整了 RestTemplate 线程池的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultSocketConfig(SocketConfig.custom().setTcpNoDelay(true).build());
connectionManager.setDefaultMaxPerRoute(20);
connectionManager.setMaxTotal(50); // 总的最大连接数
HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(connectionManager).build();

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setReadTimeout(1000 * 60 * 10);//单位为ms
factory.setConnectTimeout(1000 * 60 * 10);//单位为ms
return factory;
}

结果错误继续

调整策略

  1. 通过 jmap -heap 查看堆信息,无有效信息
  2. 通过 jmap -histo:live PID,没有实时的内存占用数据
  3. 增加内存溢出 dump,启动参数增加 -XX:+HeapDumpOnOutOfMemoryError
  4. 在发生 OutOfMemoryError 的时候,会生成.hprof 文件
  5. 导入 jvisualVm,但是由于生成的文件大小超过 2G,导入不成功
  6. 下载 mat,导入
    1
    2
    3
    4
    5
    6
    One instance of com.mysql.jdbc.JDBC4ResultSet loaded by org.springframework.boot.loader.LaunchedURLClassLoader @ 0x83000000 occupies 1,428,354,504 (68.50%) bytes. The memory is accumulated in one instance of java.lang.Object[], loaded by <system class loader>, which occupies 1,428,349,248 (68.50%) bytes.
    Keywords
    com.mysql.jdbc.JDBC4ResultSet
    org.springframework.boot.loader.LaunchedURLClassLoader @ 0x83000000
    java.lang.Object[]

  7. 发现 JDBC4ResultSet 对象占用了差不多 70%的内存。怀疑是查询的时候把所有数据都查出来了
  8. 检查代码逻辑
    1
    2
    3
       Video video = new Video();
    video.setVideoId(getVideoId(hotVideoDto.getUrl()));
    Optional<Video> videoOptional = videoRepository.findOne(Example.of(video));
    这段代码是通过 video 的 key 去数据库查找数据,但是由于 getVideoId 方法在解析失败之后,会返回 null,而 jpa 对于空字段不会带上查询,所以整个查询变成了全表扫描,这张表大概 40 万的数据,直接把内存撑满了,所以内存溢出
  9. 修改逻辑
    1
    2
    3
    4
    5
    6
    7
       Video video = new Video();
    video.setVideoId(getVideoId(hotVideoDto.getUrl()));
    if (StringUtils.isEmpty(video.getVideoId())) {
    log.info("not parse url,{}" + hotVideoDto.toString());
    return;
    }
    Optional<Video> videoOptional = videoRepository.findOne(Example.of(video));
  10. 观察无异常