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


免責聲明!

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



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