1. 程式人生 > >Jvm啟動,關閉及對應鉤子

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關閉前,執行應用程式的一些掃尾工作,進一步保證應用程式可以安全的退出。