有没有办法关闭
java.net.http.HttpClient
来立即释放它所持有的资源?
它内部包含一个选择器、一个连接池和一个
Executor
(当使用默认值时)。但是它没有实现 Closeable
/AutoCloseable
。
当我将 war 文件重新部署到 Tomcat 中时,我遇到了类似的问题。 War 应用程序有一个 HttpClient,它正在运行发出 http 请求并处理结果的计划作业。
在开发环境中重新部署 war 文件时,我经常看到 Tomcat 发出的关于挂起线程可能导致内存泄漏的警告。堆栈跟踪指向 HttpClient 线程。经过多次尝试,我通过以下方式解决了这个问题:
HttpClient 仅在需要执行作业时创建。它不是作为类或 serivec 的字段创建的,仅作为计划方法内的局部变量创建。
HttpClient 是使用构建器创建的,并填充有 ThreadPool Executor,因此我保留到 Executor 的链接并对其进行控制。
ExecutorService executor = Executors.newSingleThreadExecutor();
HttpClient client = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).connectTimeout(Duration.ofSeconds(5)).executor(executor).build();
当 try-catch 块中的工作完成时,finally 部分有这两行:显式关闭线程池并将 httpClient 局部变量设置为 null:
executor.shutdownNow();
client = null;
System.gc();
注意,有较短的连接超时时间来限制执行时间。保持较小的线程数量。我使用 1 个线程的 threadPool。
完成所有这些更改后,有关内存泄漏的警告从 Tomcat 日志中消失了。
根据 Java 21,
HttpClient
实现 AutoCloseable
。它还获得了方法 shutdown()
、shutdownNow()
和 awaitTermination()
,其工作方式与 ExecutorService
上的同名方法类似。
shutdownNow()
应该可以满足您的需求。
正如您所注意到的,
java.net.http.HttpClient
不实现 Closeable
或 AutoCloseable
。所以我只能想到两个选择,但它们都不是真正的防弹,甚至都不是好的:
您可以消除程序所持有的HttpClient
的所有
强引用,并请求垃圾收集。然而,存在一个真正的风险,即超出您直接控制范围的东西正在保留它或其组件之一。任何剩余的强引用都会阻止被引用的对象及其持有强引用的任何对象被垃圾收集。尽管如此,这可以说是比其他选择更惯用的选择。
我还找到了另一个选择。
final class HttpClientImpl extends HttpClient implements Trackable {
...
// Called from the SelectorManager thread, just before exiting.
// Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
// that may be still lingering there are properly closed (and their
// possibly still opened SocketChannel released).
private void stop() {
// Clears HTTP/1.1 cache and close its connections
connections.stop();
// Clears HTTP/2 cache and close its connections.
client2.stop();
}
...
}
除非我别无选择,否则我不会觉得使用这个很舒服。您的引用可能属于
HttpClient
类型,因此您需要将其转换为 HttpClientImpl
。依赖具体的实现而不是 HttpClient
接口是不好的,具体的实现可能会在未来的版本中发生变化。该方法也是私有的。有一些解决这个问题的方法,但很混乱。
在 Java 11 中,每个
HttpClient
都会生成一个名为 selmgr
的守护线程,该线程应该处理飞行请求。当代码中没有引用HttpClient
时,该线程将被关闭。然而,根据我的经验,它并不可靠。特别是当您使用具有未来超时的异步方法时。
这是我使用反射编写的一段代码来可靠地关闭
HttpClient
static void shutDownHttpClient(HttpClient httpClient)
{
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) httpClient.executor().get();
threadPoolExecutor.shutdown();
try {
Field implField = httpClient.getClass().getDeclaredField("impl");
implField.setAccessible(true);
Object implObj = implField.get(httpClient);
Field selmgrField = implObj.getClass().getDeclaredField("selmgr");
selmgrField.setAccessible(true);
Object selmgrObj = selmgrField.get(implObj);
Method shutDownMethod = selmgrObj.getClass().getDeclaredMethod("shutdown");
shutDownMethod.setAccessible(true);
shutDownMethod.invoke(selmgrObj);
}
catch (Exception e) {
System.out.println("exception " + e.getMessage());
e.printStackTrace();
}
}
如您所见,这取决于实现,并且可能不适用于未来的 Java 版本。它已使用 Java 11 和 Java 12 进行了测试。
此外,您需要将
--add-opens java.net.http/jdk.internal.net.http=ALL-UNNAMED
添加到您的 java 命令中。
显然
HttpClient
的设计目的是自我管理。所以它自己负责维护连接池,缓存ttl。
在
HttpClientCode
中我们可以找到以下代码:
if (!owner.isReferenced()) {
Log.logTrace("{0}: {1}",
getName(),
"HttpClient no longer referenced. Exiting...");
return;
}
这是退出
SelectorManager
循环并清理所有资源的优雅方式。
@Override
public void run() {
...
try {
while (!Thread.currentThread().isInterrupted()) {
...
if (!owner.isReferenced()) {
Log.logTrace("{0}: {1}",
getName(),
"HttpClient no longer referenced. Exiting...");
return;
}
...
}
} catch (Throwable e) {
...
} finally {
...
shutdown();
}
}
final boolean isReferenced() {
HttpClient facade = facade();
return facade != null || referenceCount() > 0;
}
所以当你的
HttpClient
对象不会被引用时,它就会清理所有资源。
UPD:您还应该通过超时来调整您的请求
这有点晚了,但我只是想强调 Jacob G.(2018 年 12 月 25 日)的评论包含了一个对我有用的解决方案:
创建 httpClient:
myExecutorService = Executors.newCachedThreadPool();
HttpClient myHttpClient = HttpClient.newBuilder()
.executor(executor)
....
.build();
关闭:
myExecutorService.shutDown();
客户端不再需要等待 90 秒才能断开连接,而是“立即”发生。
不幸的是,使用“ExecutorService”无法删除流“HttpClient-.*-SelectorManager”。终止流的唯一不好的方法是通过名称掩码查找流并强制它们终止。所有与 HttpClient 关联的流都将被终止 (java < 21):
Thread.getAllStackTraces().keySet().forEach(t -> {
if (t.getName().matches("HttpClient-.*-SelectorManager"))
t.interrupt();
});
如果只是在应用程序生命周期结束时优雅地关闭 HttpClient,则 System.exit(0) 就可以了。
public static void main(String[] args) {
...
System.exit(0);
}
我认为它向 JVM 中的所有线程发送一个中断信号,而 HttpClient selmgr 守护进程确实会接收到该信号并自行关闭。
final class HttpClientImpl extends HttpClient implements Trackable {
...
// Main loop for this client's selector
private final static class SelectorManager extends Thread {
...
@Override
public void run() {
...
try {
...
while (!Thread.currentThread().isInterrupted()) {...}
} catch (Throwable e) {...}
finally {
...
shutdown();
}