JVM進程的優雅關閉


一、前言

JVM的關閉方式可以分為三種:
  1. 正常關閉:當最后一個非守護線程結束、或者調用了System.exit、或者通過其他特定平台的方法關閉(發送SIGINT,SIGTERM信號等)
  2. 強制關閉:通過調用Runtime.halt方法、或者是在操作系統中直接kill(發送SIGKILL信號)掉JVM進程
  3. 異常關閉:運行中遇到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。比如有殘留請求的情況,如果部分資源已關閉,那么殘留請求的執行會有異常。
 
正確流程如下:
  1. kill -12:等待10s。用戶自定義SignalHandler處理12信號,而且此時所有的資源都是正常狀態。1)告知上游該服務已關閉,不要再發請求;2)處理殘留的請求;3)其他需要正常關閉的操作。
  2. kill -15:等待10s。這時會並發無序執行注冊的ShutdownHook,進行一些資源的釋放,很有可能不需要10sJVM就退出了。
  3. 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
View Code

 

用法:./main.sh start|stop|restart|kill|status

六、參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM