源码浅析:SpringBoot main方法结束为什么程序不停止
前言
对于Java开发来说,天天都在用SpringBoot,每次启动都执行了main方法,该方法应该是最容易让人忽视的地方之一,不过几行代码,为什么执行完后JVM不结束呢?
本文以内嵌tomcat为例进行说明,并分享一些debug和画图的技巧。
原因
先说结论,是因为main方法启动了一个线程,这个线程是非daemon的,并且run方法执行的任务是TomcatWebServer.this.tomcat.getServer().await();(死循环),即非daemon线程+任务不停止=程序不退出。
debug源码
技巧
在debug时,有的源码是抽象方法,我们可以用快捷键F7跳转到具体正在执行的实现类方法,另外Alt+F9可以强制到达光标的位置。
流程
下面将debug对应的源码,有兴趣的朋友可以跟着动手试试。
SpringBoot启动入口,调用静态run方法。
/** 一般demo
* @date 2021/9/12 9:09
* @author www.cnblogs.com/theRhyme
*/
@SpringBootApplication
public class CommonDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CommonDemoApplication.class, args);
}
}
调用重载的run方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
创建SpringApplication对象调用run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
由于该run方法很长,这里只贴到与本文main方法为何不结束的代码,对整个启动流程有兴趣的可以去看这篇:SpringBoot启动原理(基于2.3.9.RELEASE版本)。这里我们注意refreshContext。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
……
refreshContext调用了一个抽象方法,我们在debug模式使用F7进入具体的实现类。
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}
这里就初始化一些资源(placeholder,beanFactory,BeanPostProcessor,MessageSource,ApplicationEventMulticaster),注意onRefresh方法。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
……
进入onRefresh,这里会创建WebServer:
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
这里是具体创建webServer的步骤,注意getTomcatWebServer。
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
创建TomcatWebServer对象。
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
设置一些属性,并执行initialize方法。
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
初始化并启动tomcat容器,然后就开起非daemon await线程。
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
创建非daemon线程设置线程名等参数并启动。
private void startDaemonAwaitThread() {
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
@Override
public void run() {
TomcatWebServer.this.tomcat.getServer().await();
}
};
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
}
至此由于awaitThread.setDaemon(false);和TomcatWebServer.this.tomcat.getServer().await();,启动该线程awaitThread后,main方法后续虽然执行完毕,但是程序不会退出。
https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
await方法
这里单独看一下TomcatWebServer.this.tomcat.getServer().await();。
该方法的Java doc:
/** * Wait until a proper shutdown command is received, then return. * This keeps the main thread alive - the thread pool listening for http * connections is daemon threads. */
指的是通过等候关闭命令这个动作来保持main线程存活,而HTTP线程作为daemon线程会在main线程结束时终止。
任务一直运行的原因:源码如下,debug会进入getPortWithOffset()的值是-1的分支(注意这里不是server.port端口号),然后会不断循环**<font style="color:#DF2A3F;">Thread.sleep( 10000 )</font>**直到发出关机指令修改**<font style="color:#DF2A3F;">stopAwait</font>**的值为**<font style="color:#DF2A3F;">true</font>**。
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if (getPortWithOffset() == -2) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if (getPortWithOffset() == -1) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
……
stopAwait的值只会在org.apache.catalina.core.StandardServer#stopAwait中被修改,源码如下:
public void stopAwait() {
stopAwait=true;
Thread t = awaitThread;
if (t != null) {
ServerSocket s = awaitSocket;
if (s != null) {
awaitSocket = null;
try {
s.close();
} catch (IOException e) {
// Ignored
}
}
t.interrupt();
try {
t.join(1000);
} catch (InterruptedException e) {
// Ignored
}
}
}
而该方法会在容器生命周期结束方法org.apache.catalina.core.StandardServer#stopInternal中被调用。
非daemon线程的意义
setDaemon介绍
上面将线程设置为非daemon线程:awaitThread.setDaemon(false)。
java.lang.Thread#setDaemon源码如下:
/**
* Marks this thread as either a {@linkplain #isDaemon daemon} thread
* or a user thread. The Java Virtual Machine exits when the only
* threads running are all daemon threads.
*
* <p> This method must be invoked before the thread is started.
*
* @param on
* if {@code true}, marks this thread as a daemon thread
*
* @throws IllegalThreadStateException
* if this thread is {@linkplain #isAlive alive}
*
* @throws SecurityException
* if {@link #checkAccess} determines that the current
* thread cannot modify this thread
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
根据上面的Java doc注释可知:标记该线程是否是daemon线程,而JVM退出仅当只剩下daemon线程。
所以非daemon线程存活,JVM是不会退出的。
例子
如下代码,我们在main方法中启动了一个非daemon线程,并且调用了阻塞方法java.io.InputStream#read()。
// https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": start");
Thread awaitThread =
new Thread("non-daemon") {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ": start");
System.in.read();
System.out.println(Thread.currentThread().getName() + ": end");
} catch (IOException e) {
e.printStackTrace();
}
}
};
awaitThread.setDaemon(false);
awaitThread.start();
System.out.println(Thread.currentThread().getName() + ": end");
}
启动程序后,再不进行键盘输入的情况下,程序不会停止,运行结果如下:
main: start
main: end
non-daemon: start
main线程结束,但是程序不退出。
-1的原因
上面留了个问题,为什么getPortWithOffset()的返回值是-1。
如下getPort()的值为-1,此时相当于直接调用了getPort()方法。
https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
@Override
public int getPortWithOffset() {
// Non-positive port values have special meanings and the offset should
// not apply.
int port = getPort();
if (port > 0) {
return port + getPortOffset();
} else {
return port;
}
}
而getPort直接取的是port属性。
@Override
public int getPort() {
return this.port;
}
注意这里的port不是我们指定的server.port这个属性,而是关闭命令监听的端口。
/**
* The port number on which we wait for shutdown commands.
*/
private int port = 8005;
为什么是8005而不是-1呢?那是在哪被修改了呢?
port属性提供的修改方式是setPort(),而使用Alt+F7找到在getServer中被修改为-1。

在server.setPort( -1 );打一个断点,重新debug,可以知道具体修改的时机。
之前我们debug过方法createWebServer,是具体创建webServer的步骤,但是我们这里要进入getWebServer。
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(getSelfInitializer());
……
配置tomca实例参数,但是要注意这里的tomcat.getService()方法。
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
内部调用getServer()。
public Service getService() {
return getServer().findServices()[0];
}
至此,就是这里就将server.setPort( -1 );。
public Server getServer() {
if (server != null) {
return server;
}
System.setProperty("catalina.useNaming", "false");
server = new StandardServer();
initBaseDir();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
// https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
server.setPort( -1 );
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}
调用链
技巧
如果我们想画一个方法本次被调用(线程内部)的流程图,那么我们可以debug进入该方法,Alt+F8执行如下代码,打印出方法调用栈对应的mermaid js 内容,然后使用文本绘图工具进行渲染。
// https://www.cnblogs.com/theRhyme
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
List<String> methodChain = Arrays.stream(stackTrace)
.filter(e -> !e.getClassName().startsWith("java.") && !e.getClassName().startsWith("jdk.") && !e.getMethodName().contains("<"))
.map(e -> e.getClassName() + "." + e.getMethodName())
.collect(Collectors.toList());
StringBuilder mermaidCode = new StringBuilder("graph TD\n");
for (int i = methodChain.size() - 1; i > 0; i--) {
mermaidCode.append(String.format(" %s --> %s\n",
methodChain.get(i),
methodChain.get(i-1)));
}
System.out.println(mermaidCode);
这种方式比较适合线程内部展示具体方法的被调用关系,可以自定义根据包名等条件过滤掉不想要展示的类,但是对于跨线程的调用却不起作用,因为原理是线程自身的调用栈。
具体内容
如图,debug到org.springframework.boot.web.embedded.tomcat.TomcatWebServer#startDaemonAwaitThread内部,执行上面的代码。

输出内容:
graph TD
org.springframework.boot.devtools.restart.RestartLauncher.run --> cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main
cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.refreshContext
org.springframework.boot.SpringApplication.refreshContext --> org.springframework.boot.SpringApplication.refresh
org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.SpringApplication.refresh
org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh --> org.springframework.context.support.AbstractApplicationContext.refresh
org.springframework.context.support.AbstractApplicationContext.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize
org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread
org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread --> idea.debugger.rt.GeneratedEvaluationClass.invoke
把内容放入文本绘图中,即可得到如下流程图:
源码浅析:SpringBoot main方法结束为什么程序不停止的更多相关文章
- external-attacher源码分析(1)-main方法与启动参数分析
更多 ceph-csi 其他源码分析,请查看下面这篇博文:kubernetes ceph-csi分析目录导航 摘要 ceph-csi分析-external-attacher源码分析.external- ...
- 如果是多个 c 代码的源码文件,编译方法如下: $ gcc test1.c test2.c -o main.out $ ./main.out test1.c 与 test2.c 是两个源代码文件。
如果是多个 c 代码的源码文件,编译方法如下: $ gcc test1.c test2.c -o main.out $ ./main.out test1.c 与 test2.c 是两个源代码文件.
- spring源码浅析——IOC
=========================================== 原文链接: spring源码浅析--IOC 转载请注明出处! ======================= ...
- kernel(二)源码浅析
目录 kernel(二)源码浅析 建立工程 启动简析 head.s 入口点 查询处理器 查询机器ID 启动MMU 其他操作 start_kernel 处理命令行 分区 title: kernel(二) ...
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- String 源码浅析————终结篇
写在前面 说说这几天看源码的感受吧,其实 jdk 中的源码设计是最值得进阶学习的地方.我们在对 api 较为熟悉之后,完全可以去尝试阅读一些 jdk 源码,打开 jdk 源码后,如果你英文能力稍微过得 ...
- Bytom侧链Vapor源码浅析-节点出块过程
Bytom侧链Vapor源码浅析-节点出块过程 在这篇文章中,作者将从Vapor节点的创建开始,进而拓展讲解Vapor节点出块过程中所涉及的源码. 做为Vapor源码解析系列的第一篇,本文首先对Vap ...
- spring初始化源码浅析之关键类和扩展接口
目录 1.关键接口和类 1.1.关键类之 DefaultListableBeanFactory 1.2.关键类之XmlBeanDefinitionReader 1.3.关键类之ClassPathXml ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
随机推荐
- 将查询集SQL-存为物理 OR 临时表
最近的BI项目, 就是会涉及大量的 sql, 后台处理也全是 sql 来拼接成一张物理宽表, 然后前台也是用 sql 来做各种图形骚操作. 尤其是后台处理部分, 大量的sql, 有点尴尬的事情是, s ...
- 那些神奇的CSS特性,你都有用过么?
@charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...
- 网络编程:C10K问题
C10K问题 C10K问题就是如何一台物理机上同时服务10000个用户?C代表并发,10K就是10000 C10K 问题是由一个叫 Dan Kegel 的工程师提出并总结归纳的,你可以通过访问http ...
- C++ decltype类型推导
1.decltype介绍 decltype(declare type,声明类型)为C++11 新增的关键字,和auto功能一样,用于在编译期间进行自动类型推导. auto和decltype关键字都可以 ...
- Scipy中的稀疏矩阵的编码方式
import numpy as np from scipy import sparse (1)COO( Coordinate) 最直观的就是COO格式.它用了1维的数组来表示2维的矩阵,每个数组的长度 ...
- Seata源码—7.Seata TCC模式的事务处理
大纲 1.Seata TCC分布式事务案例配置 2.Seata TCC案例服务提供者启动分析 3.@TwoPhaseBusinessAction注解扫描源码 4.Seata TCC案例分布式事务入口分 ...
- 循环神经网络(RNN)模型
一.概述 循环神经网络(Recurrent Neural Network, RNN)是一种专门设计用于处理序列数据(如文本.语音.时间序列等)的神经网络模型.其核心思想是通过引入时间上的循环连接, ...
- [CRCI2008-2009] CVJETICI
[CRCI2008-2009] CVJETICI 观察图片及样例一: 注:下文中的被占领,指的是在这一个区间内,才有交叉开花的可能. 第一张小图发现 $2 \sim 3$ 被占领. 第二张小图发现 $ ...
- FastAPI权限验证依赖项究竟藏着什么秘密?
title: FastAPI权限验证依赖项究竟藏着什么秘密? date: 2025/06/12 06:53:53 updated: 2025/06/12 06:53:53 author: cmdrag ...
- k8s pod command使用
简单说明 我们启pod服务时,有时需要在服务启动前做一些初始化的工作,这里可能会涉及多个shell命令以及判断执行,这里可以参考下面的步骤进行: command: ["/bin/bash&q ...