关闭Java HTTP客户端。

29

有没有一种方法可以关闭java.net.http.HttpClient以立即释放它所持有的资源?

它内部持有一个选择器、一个连接池和一个 Executor(当使用默认执行器时)。但是它并没有实现Closeable/AutoCloseable接口。


1
我认为它最多只能让你获取Executor(如果存在),然后你可以关闭它。 - Jacob G.
1
JDK中的HttpClientImpl似乎是一个相关的研究方向,但似乎没有公共API来管理它,除非您指定自定义Executor。关于Http/2协议的一些相关阅读可以参考When does a http2 TCP connection close?Connection Management - Naman
7个回答

9

当我重新部署war文件到Tomcat时,遇到了类似的问题。War应用程序有一个HttpClient,它运行计划任务发出http请求并处理结果。

在开发环境中,当重新部署war文件时,我经常会看到来自Tomcat的警告,提示可能会导致内存泄漏的挂起线程。堆栈跟踪指向了HttpClient线程。经过几次尝试,我以以下方式解决了这个问题:

  1. 仅在必要时执行作业时才创建HttpClient。它不是作为类或服务的字段创建的,而是作为调度方法内的局部变量创建的。

  2. 使用构建器创建HttpClient,并填充ThreadPool Executor,因此我保留了Executor的链接并对其进行控制。 executor = Executors.newSingleThreadExecutor(); HttpClient client = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).connectTimeout(Duration.ofSeconds(5)).executor(executor).build();

  3. 当try-catch块中的作业完成后,finally部分包含以下两行:显式关闭线程池并将null设置为httpClient局部变量: executor.shutdownNow(); client = null; System.gc();

注意,设置短连接超时以限制执行时间。保持线程数较小。我使用了1个线程的线程池。

所有这些更改后,Tomcat日志中的关于内存泄漏的警告都消失了。


3
根据 Java 21,HttpClient 实现了 AutoCloseable。它还获得了 shutdown()shutdownNow()awaitTermination() 方法,其工作方式类似于 ExecutorService 上的同名方法。 shutdownNow() 应该满足您的需求。

3

如您所见,java.net.http.HttpClient没有实现CloseableAutoCloseable。因此我只能想到两个选项,但它们都不是真正的弹性或好的选项:

您可以消除程序持有的所有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.net.http.HttpClient在Java 21或更高版本中已经实现了Closeable或AutoCloseable接口。 - undefined

2
在Java 11中,每个HttpClient都会生成一个名为selmgr的守护线程,该线程应该负责处理在飞行中的请求。当代码中没有对HttpClient的引用时,这个线程将被关闭。然而,在我的经验中,它并不可靠。特别是当您使用具有future超时的异步方法时。
以下是我使用反射编写的代码片段,可可靠地关闭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上进行了测试。

另外,您需要在java命令中添加--add-opens java.net.http/jdk.internal.net.http=ALL-UNNAMED


2

很明显,HttpClient是设计为自管理的。因此,它负责维护连接池、缓存ttl等。

HttpClientCode中,我们可以找到以下代码:

Original Answer翻译成"最初的回答"

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对象没有被引用时,它会清理所有资源。
更新:同时,你应该通过传递超时时间来调整请求。

1
这有点晚了,但我想强调一下Jacob G.(2018年12月25日)的评论包含了一个对我有效的解决方案:
创建httpClient:
myExecutorService = Executors.newCachedThreadPool();
HttpClient myHttpClient = HttpClient.newBuilder() 
    .executor(executor) 
    ....
    .build();

正在关闭:

myExecutorService.shutDown();

不必等待客户端断开连接90秒,现在可以“即时”完成。


-1
如果只是为了在应用程序生命周期结束时优雅地关闭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();
            }

2
这是终止JVM而不是HttpClient。这不是HttpClient的“优雅”关闭,甚至不是JVM的优雅关闭! - Edwin Buck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接