JVM 线程与进程,主线程

前言

经常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. 主线程运行

  1. 主线程运行结束,子线程运行,可以看到主线程已经销毁了

  1. 进程结束

由此可见,所有线程运行结束,进程自动退出

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线程加载业务,当定位启动阻塞的时候就需要看日志,找到相关的线程,其他容器同理。

Guess you like

Origin juejin.im/post/7040338065444995109