问题
线上的一个应用在长时间运行后,内存使用率不断上升,堆内存无法被回收。
问题定位
使用 Arthas 导出堆转储文件,通过 Eclipes MAT 进行分析,发现 Finalizer 队列对象异常堆积。
进一步分析 Finalizer 队列的引用路径,发现 PoolingHttpClientConnectionManager 实例未被及时回收,导致大量 HTTP 连接对象残留。
通过分析PoolingHttpClientConnectionManager中的 conn 内容,定位到了使用了 Apache HttpClient4 代码行。
问题分析
在分析以上问题前,先通过出现问题的代码入手。
private static String doPostByJSON(String url, Map<String, String> headers, JSONObject data, String encoding) throws IOException {
String resultJson = null;
CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse response = null;
HttpPost httpPost = new HttpPost();
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
httpPost.setConfig(requestConfig);
try {
httpPost.setURI(new URI(url));
packageHeader(headers, httpPost);
httpPost.setEntity(new StringEntity(JSON.toJSONString(data), encoding));
response = client.execute(httpPost);
int status = response.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK) {
System.out.println("响应失败,状态码:" + status);
}
resultJson = EntityUtils.toString(response.getEntity(), encoding);
} catch (Exception e) {
e.printStackTrace();
} finally {
release(response, client);
}
return resultJson;
}
根据以上代码,初步判断出几个问题点:
HttpClients.createDefault()每次调用都会创建一个新的CloseableHttpClient实例,没有显示关闭,增加内存消耗。RequestConfig是线程安全的,应该复用同一个实例,而不是在每次请求时重新创建,避免不必要的对象分配。CloseableHttpResponse没有显示关闭,可能导致 HTTP 连接无法及时释放。
接着,分析 HttpClients.createDefault() 方法内部实现,它会构建 PoolingHttpClientConnectionManager,既然 PoolingHttpClientConnectionManager 在 Finalizer 队列中,它肯定是实现了 finalize 方法,如下:
@Override
protected void finalize() throws Throwable {
try {
shutdown();
} finally {
super.finalize();
}
}
这个 finalize() 方法的作用是在对象被 GC 回收时,主动关闭连接池,释放连接资源。然而,JVM 对 finalize() 的处理机制存在一定延迟和风险:
- JVM 维护了一个
ReferenceQueue(引用队列),当PoolingHttpClientConnectionManager实例不再被引用时,JVM 不会立即回收,而是先将其放入 Finalizer 队列。 Finalizer队列中的对象需要由 低优先级 的Finalizer线程依次执行finalize()方法,而后才能真正释放资源。- 在 GC 频繁或系统负载较高时,
Finalizer线程可能无法及时处理队列中的对象,导致PoolingHttpClientConnectionManager长时间无法释放,进而造成连接池堆积、内存泄漏。
因此,为了避免 PoolingHttpClientConnectionManager 堆积,可以通过以下手段进行优化:
- 复用
CloseableHttpClient,避免每次请求都创建新的CloseableHttpClient,而是使用单例模式进行管理,确保PoolingHttpClientConnectionManager在整个应用生命周期内被复用。 - (可选)升级到 Apache HttpClient5,它移除了
finalize()机制,不再依赖 Finalizer 线程回收资源,避免了 Finalizer 队列堆积导致的内存泄漏。即使升级到 HttpClient 5,仍然需要复用CloseableHttpClient,否则仍然会导致连接池管理不当的问题。
问题验证
通过以下 demo 代码来单独验证 Finalizer 队列堆积的问题,通过 JProfile 来根据 Finalizer 观察。
@PostMapping("post")
public int post()throws Exception {
HttpPost httpPost = new HttpPost("https://webhook.site/id");
// 设置请求头
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Content-Type", "application/json");
// 设置 JSON 请求体
String jsonBody = "{\"message\":\"Hello, Webhook!\"}";
StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
// 执行请求并获取响应
CloseableHttpResponse response = HttpClients.createDefault().execute(httpPost);
// 读取响应体
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
log.info("Response Code: {}", response.getStatusLine().getStatusCode());
log.info("Response Body: {}", responseBody);
return response.getStatusLine().getStatusCode();
}
以上每次都是通过 HttpClients.createDefault() 来创建 CloseableHttpClient,通过观察得出 Finalizer 队列持续增长。
通过复用 CloseableHttpClient 来进行验证。
@Configuration
public class HttpClient4Config {
@Bean
public CloseableHttpClient httpClient(RequestConfig requestConfig) {
return HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build();
}
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom()
.setConnectTimeout(5000) // 连接超时时间(毫秒)
.setSocketTimeout(5000) // Socket 超时时间(毫秒)
.setConnectionRequestTimeout(5000) // 从连接池获取连接的超时时间(毫秒)
.build();
}
}
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("http-client-4")
public class ApacheHttpClient4Controller {
private final CloseableHttpClient httpClient;
@PostMapping("post")
public int post()throws Exception {
HttpPost httpPost = new HttpPost("https://webhook.site/id");
// 设置请求头
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Content-Type", "application/json");
// 设置 JSON 请求体
String jsonBody = "{\"message\":\"Hello, Webhook!\"}";
StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
// 执行请求并获取响应
CloseableHttpResponse response = httpClient.execute(httpPost);
// 读取响应体
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
log.info("Response Code: {}", response.getStatusLine().getStatusCode());
log.info("Response Body: {}", responseBody);
return response.getStatusLine().getStatusCode();
}
}
观察 Finalizer 队列没有出现增长。
问题修复
- 复用
CloseableHttpClient以及RequestConfig。 - 使用 try resource 来包装该方法
httpClient.execute,在执行完之后,释放CloseableHttpResponse持有的连接。