You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaAgent中启动HttpServer导致目标JVM无法正常退出问题求解

问题背景

我在自行开发的JavaAgent中启动了一个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();

        // 现有关闭逻辑
        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();
                }
            }
        });
    // 其他插桩代码省略...
}

测试程序代码如下:

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测试程序运行完成、main线程终止后,整个JVM进程无法退出,上述代码中的关闭钩子也不会执行。如果点击Eclipse IDE的「终止」按钮,Eclipse会弹出错误,但JVM至少会退出,关闭钩子也能正常触发。

问题根因

根据java.lang.Runtime的官方文档说明:

Java虚拟机只会响应两类事件执行关闭流程:

  1. 程序正常退出:最后一个非守护线程退出,或者调用exit(等价于System.exit)方法;
  2. 虚拟机被外部中断终止:比如用户输入Ctrl+C,或者系统级事件如用户登出、系统关机。

com.sun.net.httpserver.HttpServer会默认启动一个非守护的调度线程,该线程只有在调用HttpServer#stop方法后才会退出,因此出现了死锁循环:

非守护线程未结束 → 关闭钩子不触发 → 无法调用stop停止服务 → 非守护线程持续运行无法结束

我无法修改目标应用的代码,且需要该JavaAgent兼容所有类型的Java应用,包括运行在Servlet容器中的Web应用、独立运行的JavaSE应用。


解决方案

以下两种方案均不需要修改目标应用代码,且兼容JavaSE、Web容器场景:

方案1:反射修改HttpServer调度线程为守护线程(优先推荐)

直接修改HttpServer内置非守护调度线程的属性,从根源解决非守护线程阻塞JVM退出的问题,在server.start()执行后添加以下代码即可:

try {
    // 适配OpenJDK 8/11的内置实现
    Field serverField = server.getClass().getDeclaredField("server");
    serverField.setAccessible(true);
    Object serverImpl = serverField.get(server);
    Field timerThreadField = serverImpl.getClass().getDeclaredField("timer");
    timerThreadField.setAccessible(true);
    Thread timerThread = (Thread) timerThreadField.get(serverImpl);
    timerThread.setDaemon(true);
} catch (Exception e) {
    // 反射失败时可降级使用方案2
    log.warn("modify HttpServer thread daemon status failed, fallback to watchdog mode", e);
}

注:不同OpenJDK版本的HttpServer内部字段名可能存在差异,如果反射调用失败,可以调整字段名或者直接使用方案2。

该方案优势是逻辑简单、无额外性能消耗:

  • JavaSE场景:当应用自身非守护线程全部退出后,JVM会直接触发关闭流程,执行关闭钩子完成HttpServer和线程池的资源释放
  • Web容器场景:容器本身有非守护线程维持进程运行,HttpServer的守护线程不会导致容器提前退出,容器正常关闭时也会触发钩子释放资源

方案2:新增守护看门狗线程

如果考虑跨JDK版本的兼容性,可新增一个守护线程定期检测应用非守护线程的存活状态,主动打破死锁循环,示例代码如下:

Thread watchDog = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        int appNonDaemonCount = 0;
        Set<Thread> allThreads = Thread.getAllStackTraces().keySet();
        for (Thread t : allThreads) {
            // 跳过守护线程
            if (t.isDaemon()) continue;
            String threadName = t.getName();
            // 跳过JVM内置非守护线程
            if ("DestroyJavaVM".equals(threadName)) continue;
            // 跳过JavaAgent自身的非守护线程,根据实际名称调整
            if (threadName.startsWith("JDBCLD-HTTP-SERVER") || "server-timer".equals(threadName)) continue;
            // 统计应用自身的非守护线程
            appNonDaemonCount++;
        }
        // 应用自身非守护线程全部退出,主动停止HttpServer
        if (appNonDaemonCount == 0) {
            server.stop(5);
            es.shutdown();
            break;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            break;
        }
    }
});
watchDog.setName("jdbcld-watch-dog");
watchDog.setDaemon(true);
watchDog.start();

该方案完全不依赖JDK内部实现,兼容性更强,看门狗作为守护线程本身不会阻塞JVM退出。

内容的提问来源于stack exchange,提问作者WestFarmer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 18:48:00