最簡單粗爆的方法
在Linux系統上,使用ps -aux|grep java 可以查到所有運行的java程序的pid,即進程號,然后使用kill - 9 進程號,殺死一個進程。
這樣做雖然簡單快速,但是會有一個問題,如果我們運行的服務器有緩存的數據,還沒有來得及進行持久化存儲,那么這樣操作,內存中的數據就會丟失。kill - 9是一個必殺命令,不管進程處於什么狀態,都是殺無赦,它不會給進程留下任何善后的機會。那么該如何正確的關閉游戲服務器吧?
優雅的關閉進程
優雅的關閉進程,就是在收到關閉進程的命令后,進程進行一些數據處理,比如:
- 不再接收連接
- 不再接收數據
- 把未持久化的數據進行持久化
- 清理一些臨時文件等
- 執行一些已經提交到線程池中但未執行的任務
Java進程如何接收進程停止命令
JVM 關閉鈎子
關閉鈎子本質上是一個線程(也稱為Hook線程),用來監聽JVM的關閉。通過使用Runtime的addShutdownHook(Thread hook)可以向JVM注冊一個關閉鈎子。Hook線程在JVM 正常關閉才會執行,在強制關閉時不會執行。
這個鈎子可以在一下幾種場景中被調用:
- 程序正常退出
- 使用System.exit()
- 終端使用Ctrl+C觸發的中斷
- 系統關閉
- OutOfMemory宕機
- 使用Kill pid命令干掉進程(注:在使用kill -9 pid時,是不會被調用的)
對於一個JVM中注冊的多個關閉鈎子它們將會並發執行,所以JVM並不能保證它的執行順行。當所有的Hook線程執行完畢后,如果此時runFinalizersOnExit為true,那么JVM將先運行終結器,然后停止。Hook線程會延遲JVM的關閉時間,這就要求在編寫鈎子過程中必須要盡可能的減少Hook線程的執行時間。另外由於多個鈎子是並發執行的,那么很可能因為代碼不當導致出現競態條件或死鎖等問題,為了避免該問題,強烈建議在一個鈎子中執行一系列操作。
另外在使用關閉鈎子還要注意以下幾點:
- 不能在鈎子調用System.exit(),否則卡住JVM的關閉過程,但是可以調用Runtime.halt()。
- 不能再鈎子中再進行鈎子的添加和刪掉操作,否則將會拋出IllegalStateException。
- 在System.exit()之后添加的鈎子無效。
- 當JVM收到SIGTERM命令(比如操作系統在關閉時)后,如果鈎子線程在一定時間沒有完成,那么Hook線程可能在執行過程中被終止。
- Hool線程中同樣會拋出異常,如果拋出異常又不處理,那么鈎子的執行序列就會被停止。
下面是一個簡單的示例:
public class T {
@SuppressWarnings("deprecation")
public static void main(String[] args) throws Exception {
//啟用退出JVM時執行Finalizer
Runtime.runFinalizersOnExit(true);
MyHook hook1 = new MyHook("Hook1");
MyHook hook2 = new MyHook("Hook2");
MyHook hook3 = new MyHook("Hook3");
//注冊關閉鈎子
Runtime.getRuntime().addShutdownHook(hook1);
Runtime.getRuntime().addShutdownHook(hook2);
Runtime.getRuntime().addShutdownHook(hook3);
//移除關閉鈎子
Runtime.getRuntime().removeShutdownHook(hook3);
//Main線程將在執行這句之后退出
System.out.println("Main Thread Ends.");
}
}
class MyHook extends Thread {
private String name;
public MyHook (String name) {
this.name = name;
setName(name);
}
public void run() {
System.out.println(name + " Ends.");
}
//重寫Finalizer,將在關閉鈎子后調用
protected void finalize() throws Throwable {
System.out.println(name + " Finalize.");
}
}
和(可能的)執行結果(因為JVM不保證關閉鈎子的調用順序,因此結果中的第二、三行可能出現相反的順序):
Main Thread Ends.
Hook2 Ends.
Hook1 Ends.
Hook3 Finalize.
Hook2 Finalize.
Hook1 Finalize.
可以看到,main函數執行完成,首先輸出的是Main Thread Ends,接下來執行關閉鈎子,輸出Hook2 Ends和Hook1 Ends。這兩行也可以證實:JVM確實不是以注冊的順序來調用關閉鈎子的。而由於hook3在調用了addShutdownHook后,接着對其調用了removeShutdownHook將其移除,於是hook3在JVM退出時沒有執行,因此沒有輸出Hook3 Ends。
另外,由於MyHook類實現了finalize方法,而main函數中第一行又通過Runtime.runFinalizersOnExit(true)打開了退出JVM時執行Finalizer的開關,於是3個hook對象的finalize方法被調用,輸出了3行Finalize。
注意,多次調用addShutdownHook來注冊同一個關閉鈎子將會拋出IllegalArgumentException:
Exception in thread "main" java.lang.IllegalArgumentException: Hook previously registered
at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:72)
at java.lang.Runtime.addShutdownHook(Runtime.java:211)
at T.main(T.java:12)
另外,從JavaDoc中得知:
Once the shutdown sequence has begun it can be stopped only by invoking the halt method, which forcibly terminates the virtual machine.
Once the shutdown sequence has begun it is impossible to register a new shutdown hook or de-register a previously-registered hook. Attempting either of these operations will cause an IllegalStateException to be thrown.
“一旦JVM關閉流程開始,就只能通過調用halt方法來停止該流程,也不可能再注冊或移除關閉鈎子了,這些操作將導致拋出IllegalStateException”。
如果在關閉鈎子中關閉應用程序的公共的組件,如日志服務,或者數據庫連接等,像下面這樣:
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
LogService.this.stop();
} catch (InterruptedException ignored){
//ignored
}
}
});
由於關閉鈎子將並發執行,因此在關閉日志時可能導致其他需要日志服務的關閉鈎子產生問題。為了避免這種情況,可以使關閉鈎子不依賴那些可能被應用程序或其他關閉鈎子關閉的服務。實現這種功能的一種方式是對所有服務使用同一個關閉鈎子(而不是每個服務使用一個不同的關閉鈎子),並且在該關閉鈎子中執行一系列的關閉操作。這確保了關閉操作在單個線程中串行執行,從而避免了在關閉操作之前出現競態條件或死鎖等問題。
在游戲服務器中添加關閉鈎子
public class ShutDownService {
private static CommonLog gameLogger = CommonLog.getInstance();
private static List<IShutDown> shutDownList = new ArrayList<>();
//注冊需要在關閉鈎子中執行的任務
public static void registShutDown(IShutDown shutDownServer) {
shutDownList.add(shutDownServer);
}
public static void startShutDownHook() {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
gameLogger.warn(0, "開始關閉服務器,正在清理資源.....");
for (IShutDown shutDown : shutDownList) {
if (shutDown != null) {
shutDown.shutDown();
while (!shutDown.isTerminated()) {
}
gameLogger.warn(0, "----關閉" + shutDown.getClass().getName() + "成功----");
}
}
gameLogger.warn(0, "###---服務器關閉成功---###");
}
});
}
}
Linux腳本根據端口殺死一個進程
#!/bin/bash
echo "重新啟動服務"
jar_name=GameLogicServer.jar
PROCESS=`ps -ef|grep ${jar_name} |grep -v grep|grep -v PPID|awk '{ print $2}'`
for i in $PROCESS
do
echo ######Kill the ${jar_name} process [ $i ] ########"
kill -15 $i
done
while true
do
OLD_PROCESS=`ps -ef|grep ${jar_name} |grep -v grep|grep -v PPID|awk '{ print $2}'`
if [ "${OLD_PROCESS}" = "" ]
then
echo "${PROCESS}進程已殺死成功,開始啟動新的進程"
break
else
echo "正在等待${PROCESS}進程關閉....."
sleep 1s
fi
done
nohup java -server -agentpath:/usr/jprofiler9/jprofiler9/bin/linux-x64/libjprofilerti.so=port=8849,nowait -jar ${jar_name} > console.out 2>&1 &
echo "服務器啟動完成"