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虚拟机只会响应两类事件执行关闭流程:
- 程序正常退出:最后一个非守护线程退出,或者调用exit(等价于System.exit)方法;
- 虚拟机被外部中断终止:比如用户输入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

