为什么 RestTemplate 会消耗过多的内存?

Why does RestTemplate consume excessive amounts of memory?

问题

为什么 Spring 的 RestTemplate 在发送文件时使用过多的堆(尤其是 G1 Old Generation)。

上下文

我们观察到 RestTemplate to consume excessive amounts of memory when sending files via POST requests. We used Spring's WebClient 作为比较,它的行为完全正常。

我们创建了一个包含完整代码的 demo project on github。重要部分是以下片段:

private void sendFileAsOctetStream(File file) {
    final RequestEntity<FileSystemResource> request = RequestEntity.post(URI.create("http://localhost:8080/file"))
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(new FileSystemResource(file));
    restTemplate.exchange(request, void.class);
}

private void sendFileAsOctetStream(File file) {
    webClient.post()
            .uri("/file")
            .body(BodyInserters.fromResource(new FileSystemResource(file)))
            .exchange()
            .block();
}

我们在使用两种实现方式发送一个 550MB 的文件时观察了 jconsole 的内存使用情况(左边是 WebClient,右边是 RestTemplateWebClient 消耗了一个几兆字节,而 RestTemplate 需要 2.7 GB:

  1. 用于清理老年代的初始手动 GC
  2. 要求
  3. 一次手动GC(仅适用于RestTemplate

这是因为默认 RestTemplate 仅使用未配置的 SimpleClientHttpRequestFactory 来创建请求。

提到的请求工厂有一个标志 bufferRequestBody,默认设置为 true,这会导致发送大请求时内存消耗非常高。

来自 SimpleClientHttpRequestFactory#setBufferRequestBody() 的 javadoc:

Indicate whether this request factory should buffer the request body internally. Default is true. When sending large amounts of data via POST or PUT, it is recommended to change this property to false, so as not to run out of memory. This will result in a ClientHttpRequest that either streams directly to the underlying HttpURLConnection (if the Content-Length is known in advance), or that will use "Chunked transfer encoding" (if the Content-Length is not known in advance).

您可以提供自己的请求工厂,在使用其他重载构造函数之一创建 RestTemplate 时,并在请求工厂上将提到的标志设置为 false

@Bean
public RestTemplate restTemplate() {
    SimpleClientHttpRequestFactory rf = new SimpleClientHttpRequestFactory();
    rf.setBufferRequestBody(false);
    return new RestTemplate(rf);
}