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

六、参考

posted @ 2020-05-13 20:39  waterystone  阅读(3067)  评论(0编辑  收藏  举报