一、前言
JVM的關閉方式可以分為三種:
- 正常關閉:當最后一個非守護線程結束、或者調用了System.exit、或者通過其他特定平台的方法關閉(發送SIGINT,SIGTERM信號等)
- 強制關閉:通過調用Runtime.halt方法、或者是在操作系統中直接kill(發送SIGKILL信號)掉JVM進程
- 異常關閉:運行中遇到RuntimeException異常、OOM錯誤等。
請尊重作者勞動成果,轉載請標明原文鏈接(原文持續更新,建議閱讀原文):https://www.cnblogs.com/waterystone/p/12884761.html
二、ShutdownHook
通常JVM可使用runtime.addShutdownHook()對退出信號做處理,它讓我們在程序正常退出或者發生異常時能有機會做一些清場工作。
關閉鈎子其實可以看成是一個已經初始化了的但還沒啟動的線程,
當JVM關閉時會並發無序地執行注冊的所有關閉鈎子。
Runtime.getRuntime().addShutdownHook(handleThread); //handleThread是信號處理線程。
ShutdownHook響應的信號如下:
- -1:如果使用了nohup則不響應;
- -2:如果使用了后台&則不響應;
- -15:都響應。
注意事項:
- 不要使用kill -9來結束進程,這樣ShutdownHook得不到執行;
- ShutdownHook要盡量短。計算機在關機前,會給所有的進程發送一個SIGTERM信號,等若干秒后就直接發送SIGKILL了;
- ShutdownHook要保證線程安全。如果多次發送信號,那么ShutdownHook被不同的線程多次執行;
三、SignalHandler
用戶可以自定義SignalHander對特定信號進行處理。
class DebugSignalHandler implements SignalHandler { public static void listenTo(String name) { Signal signal = new Signal(name); Signal.handle(signal, new DebugSignalHandler()); } public void handle(Signal signal) { System.out.println("Signal: " + signal); if (signal.toString().trim().equals("SIGTERM")) { System.out.println("SIGTERM raised, terminating..."); System.exit(1); } } }
Java對每個信號都啟動一個線程進行處理。注冊TERM信號,就啟動"SIGTERM handler" 線程。即便主線程被阻塞,信號依然可以得到處理。由於對信號的處理是多線程的,所以應保證信號處理實例SignalHandler應該是線程安全的。
四、總結
- ShutdownHook只響應1、2、15三種信號,而JVM一般用nohup...&的方式啟動,所以會忽略1、2兩種信號;
- ShutdownHook觸發時,多個鈎子會並發無序執行。如果資源關閉上有先后依賴則會有問題;
4.1 優雅關閉
由於ShutdownHook的並發無序執行,所以我們在優雅關閉時不能直接kill -15。比如有殘留請求的情況,如果部分資源已關閉,那么殘留請求的執行會有異常。
正確流程如下:
- kill -12:等待10s。用戶自定義SignalHandler處理12信號,而且此時所有的資源都是正常狀態。1)告知上游該服務已關閉,不要再發請求;2)處理殘留的請求;3)其他需要正常關閉的操作。
- kill -15:等待10s。這時會並發無序執行注冊的ShutdownHook,進行一些資源的釋放,很有可能不需要10sJVM就退出了。
- kill -9:如果kill -15還沒有終止JVM,則直接強制退出。
這里的優雅就體現在第一步的10秒kill -12,在資源都正常的情況下給業務一些時間來正常關閉服務。
4.2 示例
我們以轉轉的RPC框架ZZSCF為例,來看其是如何實現優雅關閉的。
4.2.1 kill -12
首先,我們進行kil -12並等待10秒,用戶自定義SignalHandler來處理12信號,而且此時所有的資源都是正常狀態。
4.2.2 kill -15
接着,我們進行kil -15並等待10秒。這時會並發無序執行注冊的ShutdownHook,進行一些資源的釋放,很有可能不需要10sJVM就退出了。
4.2.3 kill -9
最后,如果kill -15還沒有終止JVM,則直接強制退出。
五、啟動腳本DEMO

#!/bin/bash cd `dirname $0` ROOT_PATH=$(cd `dirname $0`;pwd) MAIN_JAR="${ROOT_PATH}/web-test.war" CONSOLE_LOG_PATH="/dev/null" #CONSOLE_LOG_PATH="${ROOT_PATH}/logs/console.log" JAVA_OPTS=" -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 " #SHUTDOWN_SIGNAL_12_WAIT is wait time in seconds for java process signal 12. -1 represents NOT sending signal 12. SHUTDOWN_SIGNAL_12_WAIT=10 #SHUTDOWN_SIGNAL_15_WAIT is wait time in seconds for java process signal 15. SHUTDOWN_SIGNAL_15_WAIT=10 # define color RED="\033[00;31m" GREEN="\033[00;32m" YELLOW="\033[00;33m" REG="\033[0m" echoColor() { color=$1 echo -e "${color}[`date '+%Y-%m-%d %H:%M:%S'`]$2${REG}" } echoRed() { echoColor ${RED} "$1" } echoGreen() { echoColor ${GREEN} "$1" } echoYellow() { echoColor ${YELLOW} "$1" } jvm_pid() { echo `ps -fe | grep ${MAIN_JAR} | grep -v grep | tr -s " "|cut -d" " -f2` } start() { pid=$(jvm_pid) #judge process is running. if [ -n "$pid" ]; then echoRed "${MAIN_JAR} is already running (pid: $pid)" return 1; fi # Start jvm echoGreen "Starting ${MAIN_JAR}" echoGreen "JAVA_OPTS:${JAVA_OPTS}" nohup java -jar ${JAVA_OPTS} ${MAIN_JAR} > ${CONSOLE_LOG_PATH} 2>&1 & status return 0 } status(){ pid=$(jvm_pid) if [ -n "$pid" ]; then echoGreen "${MAIN_JAR} is running with pid: $pid" else echoRed "${MAIN_JAR} is not running" fi } terminate() { echoRed "Terminating ${MAIN_JAR}" kill -9 $(jvm_pid) } stop() { pid=$(jvm_pid) if [ -n "$pid" ]; then echoRed "Stoping ${MAIN_JAR},pid=${pid}" if [ ${SHUTDOWN_SIGNAL_12_WAIT} -ge 0 ]; then echoRed "Send sigal 12 ..." kill -12 $pid signal12Count=0; until [ `ps -p $pid | grep -c $pid` = '0' ] || [ $signal12Count -gt ${SHUTDOWN_SIGNAL_12_WAIT} ] do echoRed "Waiting for signal 12 process(${signal12Count})"; sleep 1 let signal12Count=$signal12Count+1; done fi echoRed "Send sigal 15 ..." kill -15 $pid signal15Count=0; until [ `ps -p $pid | grep -c $pid` = '0' ] || [ $signal15Count -gt ${SHUTDOWN_SIGNAL_15_WAIT} ] do echoRed "Waiting for signal 15 process(${signal15Count})"; sleep 1 let signal15Count=$signal15Count+1; done if [ $signal15Count -gt ${SHUTDOWN_SIGNAL_15_WAIT} ]; then echoRed "Killing processes didn't stop after ${SHUTDOWN_SIGNAL_15_WAIT} seconds" terminate fi else echoRed "${MAIN_JAR} is not running" fi return 0 } case $1 in start) start ;; stop) stop ;; restart) stop start ;; status) status ;; kill) terminate ;; *) echoRed "Usage: $0 start|stop|restart|kill|status" ;; esac exit 0
用法:.
/main
.sh start|stop|restart|
kill
|status