线程在 javaagent 阻塞退出中启动

thread started in javaagent blocking exit

在我的 javagent 中,我启动了一个 HttpServer:

public static void premain(String agentArgs, Instrumentation inst) throws InstantiationException, IOException {

        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);

        server.createContext("/report", new ReportHandler());
        server.createContext("/data", new DataHandler());
        server.createContext("/stack", new StackHandler());
        ExecutorService es = Executors.newCachedThreadPool(new ThreadFactory() {
            int count = 0;
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setName("JDBCLD-HTTP-SERVER" + count++);
                return t;
            }
            
        });
        server.setExecutor(es);
        server.start();

        // how to properly close ?
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                server.stop(5);
                log.info("internal httpserver has been closed.");
                es.shutdown();
                try {
                    if (!es.awaitTermination(60, TimeUnit.SECONDS)) {
                        log.warn("executor service of internal httpserver not closing in 60 seconds");
                        es.shutdownNow();
                        if (!es.awaitTermination(60, TimeUnit.SECONDS))
                            log.error("executor service of internal httpserver not closing in 120 seconds, give up");
                    }else {
                        log.info("executor service of internal httpserver closed.");
                    }
                } catch (InterruptedException ie) {
                    log.warn("thread interrupted, shutdown executor service of internal httpserver");
                    es.shutdownNow();
                    Thread.currentThread().interrupt();
                }
            }
        });

    // other instrumention code ignored ...
}

测试程序:

public class AgentTest {

    public static void main(String[] args) throws SQLException {

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:oracle:thin:@172.31.27.182:1521/pas");
        config.setUsername("pas");
        config.setPassword("pas");

        HikariDataSource ds = new HikariDataSource(config);
        Connection c = ds.getConnection();
        Connection c1 = ds.getConnection();
        
        c.getMetaData();
        
        try {
            Thread.sleep(1000 * 60 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
            c.close();
            c1.close();
            ds.close();
        }
        
        c.close();
        c1.close();
        
        ds.close();
        
        
    }

}

当目标 jvm 退出时,我想要停止 HttpServer。但是当我的测试 java 程序完成时,主线程停止但整个 jvm 进程不会终止,上面代码中的关闭钩子不会执行。如果我在 eclipse IDE 中单击 'terminate' 按钮,eclipse 将显示错误: 但至少 jvm 会退出,并且我的关闭钩子会被调用。

根据 java.lang.Runtime 的 java 文档:

The Java virtual machine shuts down in response to two kinds of events:

The program exits normally, when the last non-daemon thread exits or when the exit (equivalently, System.exit) method is invoked, or The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.

com.sun.net.httpserver.HttpServer 将启动一个非守护程序调度程序线程,该线程将在 HttpServer#stop 被调用时退出,所以我面临死锁。

non-daemon thread not finish -> shutdown hook not triggered -> can't stop server -> non-daemon thread not finish

有什么好主意吗?请注意我无法修改定位应用程序的代码。

应用 kriegaex 的答案后更新

I added some logging to watch dog thread, and here is outputs:

2021-09-22 17:30:00.967 INFO  - Connnection@1594791957 acquired by 40A4F128987F8BD9C0EE6749895D1237
2021-09-22 17:30:00.968 DEBUG - Stack@40A4F128987F8BD9C0EE6749895D1237: 
java.lang.Throwable: 
    at com.zaxxer.hikari.pool.ProxyConnection.<init>(ProxyConnection.java:102)
    at com.zaxxer.hikari.pool.HikariProxyConnection.<init>(HikariProxyConnection.java)
    at com.zaxxer.hikari.pool.ProxyFactory.getProxyConnection(ProxyFactory.java)
    at com.zaxxer.hikari.pool.PoolEntry.createProxyConnection(PoolEntry.java:97)
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:192)
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
    at agenttest.AgentTest.main(AgentTest.java:19)
2021-09-22 17:30:00.969 INFO  - Connnection@686560878 acquired by 464555C270688B747CA211DE489B7730
2021-09-22 17:30:00.969 DEBUG - Stack@464555C270688B747CA211DE489B7730: 
java.lang.Throwable: 
    at com.zaxxer.hikari.pool.ProxyConnection.<init>(ProxyConnection.java:102)
    at com.zaxxer.hikari.pool.HikariProxyConnection.<init>(HikariProxyConnection.java)
    at com.zaxxer.hikari.pool.ProxyFactory.getProxyConnection(ProxyFactory.java)
    at com.zaxxer.hikari.pool.PoolEntry.createProxyConnection(PoolEntry.java:97)
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:192)
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
    at agenttest.AgentTest.main(AgentTest.java:20)
2021-09-22 17:30:00.971 DEBUG - Connnection@1594791957 used by getMetaData
2021-09-22 17:30:01.956 DEBUG - there is still 12 active threads, keep wathcing
2021-09-22 17:30:01.956 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true HikariPool-1 connection adder#true 
2021-09-22 17:30:02.956 DEBUG - there is still 12 active threads, keep wathcing
2021-09-22 17:30:02.956 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true HikariPool-1 connection adder#true 
2021-09-22 17:30:03.957 DEBUG - there is still 12 active threads, keep wathcing
2021-09-22 17:30:03.957 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true HikariPool-1 connection adder#true 
2021-09-22 17:30:04.959 DEBUG - there is still 12 active threads, keep wathcing
2021-09-22 17:30:04.959 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true HikariPool-1 connection adder#true 
2021-09-22 17:30:05.959 DEBUG - there is still 12 active threads, keep wathcing
2021-09-22 17:30:05.960 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true HikariPool-1 connection adder#true 
2021-09-22 17:30:06.960 DEBUG - there is still 11 active threads, keep wathcing
2021-09-22 17:30:06.960 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true 
2021-09-22 17:30:07.961 DEBUG - there is still 11 active threads, keep wathcing
2021-09-22 17:30:07.961 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true 
2021-09-22 17:30:08.961 DEBUG - there is still 11 active threads, keep wathcing
2021-09-22 17:30:08.961 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true 
2021-09-22 17:30:09.962 DEBUG - there is still 11 active threads, keep wathcing
2021-09-22 17:30:09.962 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true 
2021-09-22 17:30:10.962 DEBUG - there is still 11 active threads, keep wathcing
2021-09-22 17:30:10.963 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true main#false server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true HikariPool-1 housekeeper#true 
2021-09-22 17:30:10.976 INFO  - Connnection@1594791957 released
2021-09-22 17:30:10.976 DEBUG - set connection count to 0 by stack hash 40A4F128987F8BD9C0EE6749895D1237
2021-09-22 17:30:10.976 INFO  - Connnection@686560878 released
2021-09-22 17:30:10.976 DEBUG - set connection count to 0 by stack hash 464555C270688B747CA211DE489B7730
2021-09-22 17:30:11.963 DEBUG - there is still 10 active threads, keep wathcing
2021-09-22 17:30:11.963 DEBUG - Reference Handler#true Finalizer#true Signal Dispatcher#true server-timer#true Thread-2#false jdbcld-watch-dog#false Timer-0#true oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser#true InterruptTimer#true DestroyJavaVM#false 
2021-09-22 17:30:12.964 DEBUG - there is still 10 active threads, keep wathcing

更新

我想支持所有类型的 java 应用程序,包括 Web 应用程序 运行 servlet 容器和单独的标准 javase 应用程序。

这里有一点 MCVE 说明 ewrammer 的想法。我使用 byte-buddy-agent 小助手库来动态附加代理,以便使我的示例自包含,从 main 方法直接启动 Java 代理。我省略了 运行 这个例子所必需的 3 个简单的空操作虚拟处理程序 类。

package org.acme.agent;

import com.sun.net.httpserver.HttpServer;
import net.bytebuddy.agent.ByteBuddyAgent;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) throws IOException {
    HttpServer httpServer = HttpServer.create(new InetSocketAddress(8000), 0);
    ExecutorService executorService = getExecutorService(httpServer);
    Runtime.getRuntime().addShutdownHook(getShutdownHook(httpServer, executorService));
    // other instrumention code ignored ...
    startWatchDog();
  }

  private static ExecutorService getExecutorService(HttpServer server) {
    server.createContext("/report", new ReportHandler());
    server.createContext("/data", new DataHandler());
    server.createContext("/stack", new StackHandler());
    ExecutorService executorService = Executors.newCachedThreadPool(new ThreadFactory() {
      int count = 0;

      @Override
      public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        t.setName("JDBCLD-HTTP-SERVER" + count++);
        return t;
      }

    });
    server.setExecutor(executorService);
    server.start();
    return executorService;
  }

  private static Thread getShutdownHook(HttpServer httpServer, ExecutorService executorService) {
    return new Thread(() -> {
      httpServer.stop(5);
      System.out.println("Internal HTTP server has been stopped");
      executorService.shutdown();
      try {
        if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
          System.out.println("Executor service of internal HTTP server not closing in 60 seconds");
          executorService.shutdownNow();
          if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
            System.out.println("Executor service of internal HTTP server not closing in 120 seconds, giving up");
        }
        else {
          System.out.println("Executor service of internal HTTP server closed");
        }
      }
      catch (InterruptedException ie) {
        System.out.println("Thread interrupted, shutting down executor service of internal HTTP server");
        executorService.shutdownNow();
        Thread.currentThread().interrupt();
      }
    });
  }

  private static void startWatchDog() {
    ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    while (threadGroup.getParent() != null)
      threadGroup = threadGroup.getParent();
    final ThreadGroup topLevelThreadGroup = threadGroup;
    // Plus 1, because of the monitoring thread we are going to start right below
    final int activeCount = topLevelThreadGroup.activeCount() + 1;

    new Thread(() -> {
      do {
        try {
          Thread.sleep(1000);
        }
        catch (InterruptedException ignored) {}
      } while (topLevelThreadGroup.activeCount() > activeCount);
      System.exit(0);
    }).start();
  }

  public static void main(String[] args) throws IOException {
    premain(null, ByteBuddyAgent.install());
    Random random = new Random();
    for (int i = 0; i < 5; i++) {
      new Thread(() -> {
        int threadDurationSeconds = 1 + random.nextInt(10);
        System.out.println("Starting thread with duration " + threadDurationSeconds + " s");
        try {
          Thread.sleep(threadDurationSeconds * 1000);
          System.out.println("Finishing thread after " + threadDurationSeconds + " s");
        }
        catch (InterruptedException ignored) {}
      }).start();
    }
  }
}

如您所见,这基本上是您的示例代码,重构为几个辅助方法以提高可读性,以及新的看门狗方法。非常简单。

这会生成如下控制台日志:

Starting thread with duration 6 s
Starting thread with duration 6 s
Starting thread with duration 8 s
Starting thread with duration 7 s
Starting thread with duration 5 s
Finishing thread after 5 s
Finishing thread after 6 s
Finishing thread after 6 s
Finishing thread after 7 s
Finishing thread after 8 s
internal httpserver has been closed.
executor service of internal httpserver closed.