zoukankan      html  css  js  c++  java
  • Jvm启动,关闭及对应钩子

    很多时候应用服务启动或关闭会做一些预加载(比如缓存,定时任务启动等)或收尾处理工作(比如程序失败记录等)

    1. 首先看下Spring框架服务启动加载操作实现,直接上代码

    继承实现接口ApplicationListener就可以实现:

    import com.today.service.financereport.action.ExportReportRecordFailureAction
    import com.today.service.financereport.common.ReportThreadManager
    import com.today.service.financereport.dto.ExportReportFailureInput
    import org.slf4j.LoggerFactory
    import org.springframework.context.ApplicationListener
    import org.springframework.context.event.ContextRefreshedEvent
    import org.springframework.stereotype.Service

    /**
    * 类功能描述:容器启动监听器
    *
    * @author WangXueXing create at 18-11-20 上午9:35
    * @version 1.0.0
    */
    @Service
    class ContainerStartListener extends ApplicationListener[ContextRefreshedEvent] {
    private val logger = LoggerFactory.getLogger(getClass)
    override def onApplicationEvent(event: ContextRefreshedEvent): Unit = {
    logger.info("容器正在启动...")
    Runtime.getRuntime().addShutdownHook(new Thread(() => {
    logger.info("容器将要关闭,关闭前处理开始...")
    //1. 设置容器关闭前还未生成报表设置为导出失败
    ReportThreadManager.REPORT_THREAD_MAP.keySet().forEach { x =>
    new ExportReportRecordFailureAction(ExportReportFailureInput(x, new Throwable("容器被关闭"))).execute
    }
    logger.info("容器将要关闭,关闭前处理完成")
    }))
    }
    }

    2. 退出服务及几种退出方法

    如下图:

    对于强制关闭的几种情况,系统关机,操作系统会通知JVM进程关闭并等待,一旦等待超时,系统会强制中止JVM进程;kill -9、Runtime.halt()、断电、系统crash这些种方式会直接无商量中止JVM进程,JVM完全没有执行扫尾工作的机会。因此对用应用程序而言,我们强烈不建议使用kill -9 这种暴力方式退出。
    而对于正常关闭、异常关闭的几种情况,JVM关闭前,都会调用已注册的shutdown hooks,基于这种机制,我们可以将扫尾的工作放在shutdown hooks中,进而使我们的应用程序安全的退出。基于平台通用性的考虑,我们更推荐应用程序使用System.exit(0)这种方式退出JVM。

    JVM 与 shutdown hooks 交互流程如下图所示,可以对照源码进一步的学习shutdown hooks工作原理。
    image

    Jvm安全退出

    对于tomcat类Web应用,我们可以直接通过Runtime.addShutdownHook(Thread hook)注册自定义钩子,在钩子中实现资源的清理;而对于worker类应用,我们可以采用如下的方式安全的退出应用。

    基于信号的进程通知机制

    信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。通俗来讲,信号就是进程间的一种异步通信机制。信号具有平台相关性,Linux平台支持的一些终止进程信号如下所示:

    信号名称用途
    SIGKILL 终止进程,强制杀死进程
    SIGTERM 终止进程,软件终止信号
    SIGTSTP 停止进程,终端来的停止信号
    SIGPROF 终止进程,统计分布图用计时器到时
    SIGUSR1 终止进程,用户定义信号1
    SIGUSR2 终止进程,用户定义信号2
    SIGINT 终止进程,中断进程
    SIGQUIT 建立CORE文件终止进程,并且生成core文件

    Windows平台存在一些差异,它的一些信号举例如下所示:

    信号名称用途
    SIGINT Ctrl+C中断
    SIGTERM kill发出的软件终止
    SIGBREAK Ctrl+Break中断

    信号选择:为了不干扰正常信号的运作,又能模拟Java异步通知,在Linux上我们需要先选定一种特殊的信号。通过查看信号列表上的描述,发现 SIGUSR1 和 SIGUSR2 是允许用户自定义的信号,我们可以选择SIGUSR2,在Windows上我们可以选择SIGINT。

    通过这种信号机制,对应用程序JVM发送特定信号,JVM可以感知并处理该信号,进而可以接受程序退出指令。

    安全退出实现

    首先看下通用的JVM安全退出的流程图:

    image

    第一步,应用进程启动的时候,初始化Signal实例,它的代码示例如下:

    1
    
    Signal sig = new Signal(getOSSignalType());
    

    其中Signal构造函数的参数为String字符串,也就上文介绍的信号量名称。

    第二步,根据操作系统的名称来获取对应的信号名称,代码如下:

    1
    2
    3
    4
    5
    
    private String getOSSignalType()
       {
           return System.getProperties().getProperty("os.name").
                     toLowerCase().startsWith("win") ? "INT" : "USR2";
        }
    

    判断是否是windows操作系统,如果是则选择SIGINT,接收Ctrl+C中断的指令;否则选择USR2信号,接收SIGUSR2(等价于kill -12 pid)指令。

    第三步,将实例化之后的SignalHandler注册到JVM的Signal,一旦JVM进程接收到kill -12 或者 Ctrl+C则回调handle接口,代码示例如下:

    1
    
    Signal.handle(sig, shutdownHandler);
    

    其中shutdownHandler实现了SignalHandler接口的handle(Signal sgin)方法,代码示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public class ShutdownHandler implements SignalHandler {
        /**
         * 处理信号
         *
         * @param signal 信号
         */
        public void handle(Signal signal) {
        }
    }
    

    第四步,在接收到信号回调的handle接口中,初始化JVM的ShutdownHook线程,并将其注册到Runtime中,示例代码如下:

    1
    2
    3
    4
    5
    
    private void registerShutdownHook()
     {
            Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread");
            Runtime.getRuntime().addShutdownHook(t);
     }
    

    第五步,接收到进程退出信号后,在回调的handle接口中执行虚拟机的退出操作,示例代码如下:

    1
    
    Runtime.getRuntime().exit(0);
    

    JVM退出时,底层会自动检测用户是否注册了ShutdownHook任务,如果有,则会自动执行注册钩子的Run方法,应用只需要在ShutdownHook中执行扫尾工作即可,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    class ShutdownHook implements Runnable
    {
            @Override
            public void run() {
                    System.out.println("ShutdownHook execute start...");
                    try {
                       TimeUnit.SECONDS.sleep(10);//模拟应用进程退出前的处理操作
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("ShutdownHook execute end...");
                    }
    }
    

    通过以上的几个步骤,我们可以轻松实现JVM的安全退出,另外,通常安全退出需要有超时控制机制,例如30S,如果到达超时时间仍然没有完成退出,则由停机脚本直接调用kill -9强制退出。

    使用关闭钩子的注意事项

    • 关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,所以JVM并不保证它们的执行顺序;由于是并发执行的,那么很可能因为代码不当导致出现竞态条件或死锁等问题,为了避免该问题,强烈建议在一个钩子中执行一系列操作。

    • Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程中必须要尽可能的减少Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操作。

    • 关闭钩子执行过程中可能被强制打断,比如在操作系统关机时,操作系统会等待进程停止,等待超时,进程仍未停止,操作系统会强制的杀死该进程,在这类情况下,关闭钩子在执行过程中被强制中止。
    • 在关闭钩子中,不能执行注册、移除钩子的操作,JVM将关闭钩子序列初始化完毕后,不允许再次添加或者移除已经存在的钩子,否则JVM抛出 IllegalStateException。
    • 不能在钩子调用System.exit(),否则卡住JVM的关闭过程,但是可以调用Runtime.halt()。
    • Hook线程中同样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常,不会影响其他hook线程以及JVM正常退出。

    总结

    为了保障应用重启过程中异步操作的执行,避免强制退出JVM可能产生的各种问题,我们可以采用关闭钩子、自定义信号的方式,主动的通知JVM退出,并在JVM关闭前,执行应用程序的一些扫尾工作,进一步保证应用程序可以安全的退出。

  • 相关阅读:
    OC内存管理
    摘要算法
    加密算法
    编码技术
    Golang遇到的一些问题总结
    SignalR
    uni-app 小程序 vue
    C# 调用 C++ dll的两种方式
    Vue 项目 VSCode 调试
    Navicat 导出 表结构
  • 原文地址:https://www.cnblogs.com/barrywxx/p/10000529.html
Copyright © 2011-2022 走看看