Использование net.http.HttpClient в Tomcat вызывает утечку памяти

Я использую новый Java (начиная с версии 11) HttpClient в веб-приложении на основе сервлета:

private static final HttpClient HTTP_CLIENT =
  .connectTimeout(Duration.ofSeconds(5))
  .followRedirects(HttpClient.Redirect.NORMAL)
  .build();

...

public void httpPostAsyncToEndpoint(WebEndpoint endpoint, Map<String,String> params) {
  HttpRequest req = buildRequest(endpoint, params);
  CompletableFuture<HttpResponse<String>> future = HTTP_CLIENT.sendAsync(req, HttpResponse.BodyHandlers.ofString());
  future.thenAccept((HttpResponse<String> res) -> {
    if (res.statusCode() >= 400) {
      if (LOGGER.isErrorEnabled()) {
        LOGGER.error("{} HTTP response returned from endpoint {}", endpoint, res.statusCode());
      }
    }
  }).exceptionally(ex -> {
    if (LOGGER.isErrorEnabled()) {
      LOGGER.error("Could not audit event using endpoint {}", endpoint, ex);
    }
    return null;
  });
}

Все работает отлично, за исключением того, что при перезапуске веб-приложения на Tomcat выдается следующее предупреждение:

14-Aug-2020 09:21:16.996 WARNING [http-nio-8080-exec-18] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [MyApp] appears to have started a thread named [HttpClient-3-SelectorManager] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.base/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(Native Method)
 java.base/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:357)
 java.base/sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:182)
 java.base/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)
 java.base/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:136)
 java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:867)

Как я могу предотвратить это? Я попытался использовать пользовательский ThreadFactory, который возвращает только потоки демона:

HttpClient.newBuilder()
  .executor(Executors.newSingleThreadExecutor((Runnable r) -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
  }))
  .connectTimeout(Duration.ofSeconds(5))
  .followRedirects(HttpClient.Redirect.NORMAL).build();

но предупреждение сохраняется.

Я использую OpenJDK 11.0.7 на Tomcat 9.


person Brice Roncace    schedule 14.08.2020    source источник


Ответы (2)


Поток диспетчера селекторов будет оставаться активным до тех пор, пока жива ссылка на HttpClient или пока операция, инициированная клиентом, все еще выполняется. Потоку может потребоваться несколько секунд, чтобы обнаружить, что HttpClient больше не упоминается. Поэтому я не верю, что то, что у вас есть, является фактической утечкой, если только класс, содержащий статическую ссылку на HttpClient, не остается закрепленным в памяти по другим причинам.

person daniel    schedule 17.08.2020
comment
Спасибо @daniel, я ценю ваш опыт. См. также stackoverflow.com/a/28106807/225217. - person Brice Roncace; 18.08.2020

Вызов setDaemon в вашем потоке не поможет, поскольку виртуальная машина Tomcat не выключается. Вы должны добавить код, чтобы попытаться очистить службу-исполнитель, создавшую поток.

Назначьте службу-исполнитель статической переменной и вызовите shutdown() из сервлета destroy(), чтобы попытаться запустить очистку, когда веб-приложение не развернуто:

private static final ExecutorService SERVICE = Executors.newSingleThreadExecutor((Runnable r) -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
  });

public void destroy() {
    SERVICE.shutdown();
    super.destroy();
}

HttpClient.newBuilder().executor(SERVICE) ...

Вам нужно будет решить, какой из shutdown() или awaitTermination() лучше, если вы хотите выполнить какие-либо незавершенные задачи.

person DuncG    schedule 15.08.2020
comment
Спасибо за ответ. Я сделал, как вы предложили, вызывая shutdown() и даже shutdownNow() в методе @WebListener contextDestroyed, но Tomcat продолжает выводить одно и то же предупреждение. На самом деле, даже если я создаю HttpClient локально для каждого вызова, я получаю одно и то же предупреждение Tomcat! Начинаю думать, что это просто общее предупреждение Tomcat, которое я могу спокойно игнорировать. - person Brice Roncace; 17.08.2020