### 先说结论:为什么 `IdleConnectionEvictor` 会引发线程泄漏? 在 Apache HttpClient 5 里,`evictIdleConnections(...)` 会启动一个后台 “evictor 线程”: * **每次创建 [CloseableHttpClient](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 时都会启动一个 evictor 线程**(内部是一个 `ScheduledExecutorService` / [Thread](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)) * 这个线程 **不依赖 JVM GC**,只要 [CloseableHttpClient](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 不被 [close()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html),线程就一直存活 * 如果你频繁 [new RestTemplate()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)(或频繁 [new CloseableHttpClient()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)),就会产生 **无限个 evictor 线程** 这类线程属于 **资源泄漏的一种**:看起来程序没报错,但线程数会线性增长,最终会因为系统线程/文件句柄/内存用完而挂掉(你碰到的 `OutOfMemoryError: Failed to create a thread` 就是典型症状)。 [font color="red"]BTW 就算没有evictIdleConnections,默认机制2小时会清理僵尸连接,如果只是和几个服务之间的http请求,就不必担心。[/font] ## 背景 任务是通过jar分发,**通过反射/类加载器每次都 `new` 出来**,这意味着“构造器里 new RestTemplate”这种写法会随着每次 load 产生新的 RestTemplate、新的 CloseableHttpClient、新的连接池(和新的后台 evictor 线程)。 但是我在创建对象时使用的是以下代码,CloseableHttpClient是封装在RestTemplate里面的,无法直接close: ```java private RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 应改为 ```java private static final RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 这样即便反复new 对象,所有实例都共享同一个 RestTemplate(及它的连接池/HttpClient),连接池才会复用、线程才不会一直长起来。 ## 理解resttemplate工作原理 我在想如果不用static的话,会发生什么?首先看看RestTemplate的实现原理: ```plaintext ┌─────────────────────────────────────────┐ │ RestTemplate │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │Message │ │Message │ │ 各类 │ │ │ │Converter│ │Converter│ │ Interceptor│ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ └─────────────┴───────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ ClientHttpRequestFactory │ │ │ │ (Simple/Apache HttpClient/OkHttp)│ │ │ └─────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ HTTP Client (底层实现) │ │ │ │ (JDK HttpURLConnection/Apache/OkHttp)│ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` | 步骤 | 组件 | 作用 | | ---- | ------------------------------ | ---------------------------- | | 1 | `UriTemplateHandler` | 解析URI模板,填充变量 | | 2 | `ClientHttpRequestInterceptor` | 执行拦截器链(认证、日志等) | | 3 | `ClientHttpRequestFactory` | 创建HTTP请求对象 | | 4 | `MessageConverter` | 序列化请求体/反序列化响应 | | 5 | `ResponseErrorHandler` | 处理错误响应 | ## 如何复用连接池 先配置 ```java /** * 获取绕过 SSL 验证的 RestTemplate 实例(仅限开发/测试环境) * @return RestTemplate 实例 * @throws RuntimeException 如果 SSL 配置失败 */ public static RestTemplate getInsecureRestTemplate() { try { // 创建信任所有证书的 SSLContext SSLContext sslContext = SSLContextBuilder.create() .loadTrustMaterial((chain, authType) -> true) // 信任所有证书 .build(); // 创建 SSLConnectionSocketFactory,禁用主机名验证 SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE); // 创建 HTTP 和 HTTPS 的连接工厂注册表 Registry socketFactoryRegistry = RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", sslSocketFactory) .build(); // 创建连接管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 单一目标宿主并发连接数 connectionManager.setDefaultSocketConfig( org.apache.hc.core5.http.io.SocketConfig.custom() .setSoKeepAlive(true) // TCP Keep-Alive,保持长连接 .build()); // 创建 HttpClient CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build(); // 创建 HttpComponentsClientHttpRequestFactory 并设置超时 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(3000); // 连接超时 3s factory.setReadTimeout(10000); // 读取超时 10s factory.setConnectionRequestTimeout(2000); // 从连接池获取连接超时 2s // 返回配置好的 RestTemplate return new RestTemplate(factory); } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { throw new RuntimeException("Failed to configure RestTemplate with insecure SSL", e); } } ``` 使用的时候注意,只new一次,然后复用该对象,即可实现连接池复用 复用的前提是**使用同一个HttpClient实例** ```java RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 可以把这个restTemplate做成单例也可以用static final修饰都行,只要保证只new一次然后复用该对象即可 Loading... ### 先说结论:为什么 `IdleConnectionEvictor` 会引发线程泄漏? 在 Apache HttpClient 5 里,`evictIdleConnections(...)` 会启动一个后台 “evictor 线程”: * **每次创建 [CloseableHttpClient](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 时都会启动一个 evictor 线程**(内部是一个 `ScheduledExecutorService` / [Thread](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)) * 这个线程 **不依赖 JVM GC**,只要 [CloseableHttpClient](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 不被 [close()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html),线程就一直存活 * 如果你频繁 [new RestTemplate()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)(或频繁 [new CloseableHttpClient()](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)),就会产生 **无限个 evictor 线程** 这类线程属于 **资源泄漏的一种**:看起来程序没报错,但线程数会线性增长,最终会因为系统线程/文件句柄/内存用完而挂掉(你碰到的 `OutOfMemoryError: Failed to create a thread` 就是典型症状)。 <span style='color:red'>BTW 就算没有evictIdleConnections,默认机制2小时会清理僵尸连接,如果只是和几个服务之间的http请求,就不必担心。</span> ## 背景 任务是通过jar分发,**通过反射/类加载器每次都 `new` 出来**,这意味着“构造器里 new RestTemplate”这种写法会随着每次 load 产生新的 RestTemplate、新的 CloseableHttpClient、新的连接池(和新的后台 evictor 线程)。 但是我在创建对象时使用的是以下代码,CloseableHttpClient是封装在RestTemplate里面的,无法直接close: ```java private RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 应改为 ```java private static final RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 这样即便反复new 对象,所有实例都共享同一个 RestTemplate(及它的连接池/HttpClient),连接池才会复用、线程才不会一直长起来。 ## 理解resttemplate工作原理 我在想如果不用static的话,会发生什么?首先看看RestTemplate的实现原理: ```plaintext ┌─────────────────────────────────────────┐ │ RestTemplate │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │Message │ │Message │ │ 各类 │ │ │ │Converter│ │Converter│ │ Interceptor│ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ └─────────────┴───────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ ClientHttpRequestFactory │ │ │ │ (Simple/Apache HttpClient/OkHttp)│ │ │ └─────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ HTTP Client (底层实现) │ │ │ │ (JDK HttpURLConnection/Apache/OkHttp)│ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` | 步骤 | 组件 | 作用 | | ---- | ------------------------------ | ---------------------------- | | 1 | `UriTemplateHandler` | 解析URI模板,填充变量 | | 2 | `ClientHttpRequestInterceptor` | 执行拦截器链(认证、日志等) | | 3 | `ClientHttpRequestFactory` | 创建HTTP请求对象 | | 4 | `MessageConverter` | 序列化请求体/反序列化响应 | | 5 | `ResponseErrorHandler` | 处理错误响应 | ## 如何复用连接池 先配置 ```java /** * 获取绕过 SSL 验证的 RestTemplate 实例(仅限开发/测试环境) * @return RestTemplate 实例 * @throws RuntimeException 如果 SSL 配置失败 */ public static RestTemplate getInsecureRestTemplate() { try { // 创建信任所有证书的 SSLContext SSLContext sslContext = SSLContextBuilder.create() .loadTrustMaterial((chain, authType) -> true) // 信任所有证书 .build(); // 创建 SSLConnectionSocketFactory,禁用主机名验证 SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE); // 创建 HTTP 和 HTTPS 的连接工厂注册表 Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", sslSocketFactory) .build(); // 创建连接管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 单一目标宿主并发连接数 connectionManager.setDefaultSocketConfig( org.apache.hc.core5.http.io.SocketConfig.custom() .setSoKeepAlive(true) // TCP Keep-Alive,保持长连接 .build()); // 创建 HttpClient CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build(); // 创建 HttpComponentsClientHttpRequestFactory 并设置超时 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(3000); // 连接超时 3s factory.setReadTimeout(10000); // 读取超时 10s factory.setConnectionRequestTimeout(2000); // 从连接池获取连接超时 2s // 返回配置好的 RestTemplate return new RestTemplate(factory); } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { throw new RuntimeException("Failed to configure RestTemplate with insecure SSL", e); } } ``` 使用的时候注意,只new一次,然后复用该对象,即可实现连接池复用 复用的前提是**使用同一个HttpClient实例** ```java RestTemplate restTemplate = RestTemplateUtil.getInsecureRestTemplate(); ``` 可以把这个restTemplate做成单例也可以用static final修饰都行,只要保证只new一次然后复用该对象即可 最后修改:2026 年 03 月 17 日 © 允许规范转载 赞 别打赏,我怕忍不住购买辣条与续命水