Jvm啓動,關閉及對應鉤子

不少時候應用服務啓動或關閉會作一些預加載(好比緩存,定時任務啓動等)或收尾處理工做(好比程序失敗記錄等)spring

1. 首先看下Spring框架服務啓動加載操做實現,直接上代碼windows

繼承實現接口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. 退出服務及幾種退出方法緩存

以下圖:tomcat

對於強制關閉的幾種狀況,系統關機,操做系統會通知JVM進程關閉並等待,一旦等待超時,系統會強制停止JVM進程;kill -九、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平臺存在一些差別,它的一些信號舉例以下所示:ide

信號名稱 用途
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關閉前,執行應用程序的一些掃尾工做,進一步保證應用程序能夠安全的退出。

相關文章
相關標籤/搜索