前言
经常JVM进程启动过程中就自动退出,但是有时候却不会,笔者也没有深究原理,直到最近处理问题,发现不知道为什么进程退出。原来JVM早就定义了规范。这对我们开发中间件会提供一种设计规范。
1. 进程退出
1.1 线程执行结束进程退出
demo如下:
public class ThreadDaemon {
public static void main(String[] args) {
System.out.println("main thread start...");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("sub thread start...");
try {
Thread.sleep(10*1000);
System.out.println("sub thread run end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
复制代码
运行后:主线程执行结束,进程仍然存在,直到子线程运行结束才会退出
- 主线程运行
- 主线程运行结束,子线程运行,可以看到主线程已经销毁了
- 进程结束
由此可见,所有线程运行结束,进程自动退出
1.2 守护线程
我们上个demo创建的线程是自定义的非守护线程,这里提及守护线程,是由于守护线程的定义
/**
* 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虚拟机就会退出
The Java Virtual Machine exits when the only threads running are all daemon threads.
来试试
public class ThreadDaemon {
public static void main(String[] args) {
System.out.println("main thread start...");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("sub thread start...");
try {
Thread.sleep(10*1000);
System.out.println("sub thread run end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("sub thread start...");
try {
Thread.sleep(10*1000000000);
System.out.println("sub thread daemon... run end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread2.setDaemon(true);
thread2.start();
}
}
复制代码
这里设置为daemon true,默认为false
执行后:
仅剩daemon线程时,进程直接退出
并未打印,实际上JVM自带的一些线程也是守护线程,比如
可以看到JVM自己的回收,标记,引用线程都是守护线程,实际上我们开发基础框架或者中间件插件都建议遵循此标准,方便jvm管理线程,当然也可以自定义生命周期,就需要自己全部处理,任何环节都不能漏掉。
2. 主线程
在实际的工程运行中,主线程有时候仅用于启动的作用,比如传统Tomcat部署的应用;有时候确是核心框架的加载线程,比如spring boot的jar启动。此时如果要启动分析就需要针对设计特殊处理,不过随着Spring boot应用的大规模流行,除了比较老旧的应用,基本上都是主线程加载核心逻辑,一般而言分析主线程即可。
我启动了一个Tomcat(传统的)
打印线程
可以看到主线程
"main" #1 prio=5 os_prio=31 cpu=993.13ms elapsed=108.87s tid=0x00007fbe9700b600 nid=0x1c03 runnable [0x00007000018a2000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.Net.accept(java.base@17/Native Method)
at sun.nio.ch.NioSocketImpl.accept(java.base@17/NioSocketImpl.java:755)
at java.net.ServerSocket.implAccept(java.base@17/ServerSocket.java:675)
at java.net.ServerSocket.platformImplAccept(java.base@17/ServerSocket.java:641)
at java.net.ServerSocket.implAccept(java.base@17/ServerSocket.java:617)
at java.net.ServerSocket.implAccept(java.base@17/ServerSocket.java:574)
at java.net.ServerSocket.accept(java.base@17/ServerSocket.java:532)
at org.apache.catalina.core.StandardServer.await(StandardServer.java:602
)
at org.apache.catalina.startup.Catalina.await(Catalina.java:864)
at org.apache.catalina.startup.Catalina.start(Catalina.java:810)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(java.base@17/Na
tive Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(java.base@17/Nat
iveMethodAccessorImpl.java:77)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(java.base@17
/DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(java.base@17/Method.java:568)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
复制代码
查看源码,果然
然而嵌入式Tomcat又是另外一种情况,spring boot的应用
打印堆栈,然而找不到主线程,说明主线程销毁了
3. 主线程阻塞分析
一般而言,启动应用时阻塞,此时除非看日志,或者打标记等,很难知道是否阻塞,哪个业务阻塞了主线程。如果是非主线程启动应用,那么怎么分析了
比如传统Tomcat,写一个sleep方法
import javax.servlet.*;
import java.io.IOException;
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("------------init ------------===============");
try {
Thread.sleep(Long.parseLong("1000000000"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
}
web.xml
<filter>
<filter-name>filter</filter-name>
<filter-class>DemoFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
复制代码
启动Tomcat后,有如下日志:对于Tomcat 8
jstack可以看到是localhost-startStop-1这个线程在加载
这种就很难排查了,幸好Tomcat打印了日志,如果没有,只能看Tomcat源码
对于Tomcat9 或者 嵌入式的Tomcat
直接main方法去执行,可能Tomcat也考虑到这点了,主线程即加载线程,boot demo一个
@Component
public class DemoBean implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("--------------- init ====================");
Thread.sleep(Long.parseLong("100000000"));
}
}
复制代码
从Tomcat9开始就统一了,main线程加载框架,中间件等
总结
经过分析发现:如果运行的所有线程是守护线程,那么jvm就退出了,进程将结束。
另外Tomcat 8的传统版,不是main线程加载业务,当定位启动阻塞的时候就需要看日志,找到相关的线程,其他容器同理。