Java System#exit 無法退出程序的問題探索

背景

有朋友碰到了一個(gè)情況:java.lang.System#exit無法退出應(yīng)用程序。我聽到這種情況的時(shí)候是感覺很驚奇的,這函數(shù)還能不起作用蔑水?這就好奇不已了呀

接著,朋友繼續(xù)給出了他的場景描述:在Dubbo應(yīng)用連接注冊中心的時(shí)候扬蕊,如果連接(超時(shí))失敗搀别,期望調(diào)用System#exit退出應(yīng)用程序,但是程序并沒有按期望退出尾抑,JVM進(jìn)程還存在

與此同時(shí)歇父,如果把執(zhí)行System#exit的代碼放到另一個(gè)線程,程序可以按期望退出再愈,JVM進(jìn)程結(jié)束

用偽代碼描述如下:

Future<Object> future = 連接注冊中心的Future;
try {
    Object o = future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
    log.error("connect failed xxxx");
    System.exit(1); // 程序無法退出
}

-----------

Future<Object> future = 連接注冊中心的Future;
try {
    Object o = future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
    log.error("connect failed xxxx");
    new Thread(() -> System.exit(1)).start(); // 程序能按期望退出
}

朋友面臨的場景比偽代碼描述的情況復(fù)雜的多榜苫,但所面臨的本質(zhì)問題是一樣的。更一般化地問題翎冲,在Dubbo的org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry構(gòu)造函數(shù)中垂睬,直接執(zhí)行System.exit(1);程序無法退出,放在異步線程中執(zhí)行卻可以按期望退出

即:

// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    System.exit(1); //JVM進(jìn)程無法退出
    // ...(省略)
}

-----------
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    new Thread(() -> {System.exit(1);}).start(); //JVM進(jìn)程正常退出
    // ...(省略)
}

這就更令人驚奇了抗悍!

問題排查

要找出問題產(chǎn)生的原因羔飞,首先得有一些預(yù)備知識宵统,否則會(huì)茫然無措优妙,感覺無從下手

  1. java.lang.System#exit 方法是Java提供的能夠停止JVM進(jìn)程的方法
  2. 該方法被觸發(fā)時(shí),JVM會(huì)去調(diào)用Shutdown Hook(關(guān)閉勾子)方法盯仪,直到所有勾子方法執(zhí)行完畢疟暖,才會(huì)關(guān)閉JVM進(jìn)程

由上述第2點(diǎn)猜測:是否存在死循環(huán)的勾子函數(shù)無法退出卡儒,以致JVM沒有去關(guān)閉進(jìn)程?

舉個(gè)例子:

public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        while (true) {
            try {
                System.out.println("closing...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        }
    }));

    System.out.println("before exit...");
    System.exit(0); 
    System.out.println("after exit..."); //代碼不會(huì)執(zhí)行
}

如上俐巴,在main方法里先注冊了一個(gè)shutdown hook骨望,該勾子函數(shù)是個(gè)死循環(huán),永遠(yuǎn)也不會(huì)退出欣舵,每3秒打印一次"closing..."

接著執(zhí)行System.exit(0);方法擎鸠,期望退出JVM進(jìn)程

before exit...
closing...
closing...
closing...
closing...
closing...

...

結(jié)果是控制臺不斷打印"closing...",且JVM進(jìn)程沒有退出

原因正是上述第二點(diǎn)儲備知識提到的:JVM會(huì)等待所有勾子執(zhí)行完畢之后缘圈,才關(guān)閉進(jìn)程劣光。而示例中的shutdown hook 永遠(yuǎn)也不會(huì)執(zhí)行完畢,因此JVM進(jìn)程也不會(huì)被關(guān)閉

盡管有了儲備知識糟把,仍然很疑惑:如果存在死循環(huán)的shutdown hook绢涡,那么System.exit無論是在主線程中調(diào)用,還是在異步線程中調(diào)用遣疯,都應(yīng)該不會(huì)關(guān)閉JVM進(jìn)程雄可;反之,如果不存在死循環(huán)的shutdown hook,無論是在哪個(gè)線程調(diào)用数苫,都應(yīng)該會(huì)關(guān)閉JVM進(jìn)程聪舒。為什么在背景的偽代碼中,卻是因?yàn)椴煌恼{(diào)用線程執(zhí)行System.exit虐急,導(dǎo)致不一樣的結(jié)果呢过椎?

這時(shí)候只好想辦法,看看shutdown hook們都在偷摸干啥事戏仓,為什么未執(zhí)行完畢疚宇,以致JVM進(jìn)程不能退出

恰好對Dubbo的源碼也略有研究,很容易就找到org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry的構(gòu)造函數(shù)赏殃,并在其中加上一行代碼敷待,如下所示,改完之后重新編譯源碼仁热,并引入自己的工程中進(jìn)行Debug

注:本次使用的Dubbo版本為2.7.6

// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    System.exit(1); // 新增加的一行代碼
    // ...(省略)
}

啟動(dòng)工程榜揖,熟悉Dubbo的朋友應(yīng)該會(huì)知道,應(yīng)用啟動(dòng)的過程中會(huì)去注冊中心(這兒是Zookeeper)注冊或者訂閱抗蠢,因?yàn)閱?dòng)的是消費(fèi)者举哟,因此應(yīng)用會(huì)嘗試連接注冊中心Zookeeper,會(huì)走到ZookeeperRegistry的構(gòu)造函數(shù)迅矛,由于構(gòu)造函數(shù)第二行是新增的代碼System.exit(1);妨猩,按照背景的說法,JVM不會(huì)退出秽褒,且會(huì)卡死壶硅,這時(shí)候,借助IDEA的"快照"功能销斟,可以"拍"下Java線程棧的運(yùn)行情況庐椒,功能上相當(dāng)于執(zhí)行jstack命令

image
image

從線程棧中看出一個(gè)可疑的線程:DubboShutdownHook

從名字上可以看出是一個(gè)Dubbo注冊的一個(gè)shutdown hook,其主要目的是為了關(guān)閉連接蚂踊、做一些資源的回收等工作

從圖中也可以看出约谈,線程阻塞在org.apache.dubbo.registry.support.AbstractRegistryFactory第83行

public static void destroyAll() {
    if (!destroyed.compareAndSet(false, true)) {
        return;
    }

    if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Close all registries " + getRegistries());
    }
    // Lock up the registry shutdown process
    LOCK.lock(); // 83行,DubboShutdownHook線程阻塞在此處
    try {
        for (Registry registry : getRegistries()) {
            try {
                registry.destroy();
            } catch (Throwable e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
        REGISTRIES.clear();
    } finally {
        // Release the lock
        LOCK.unlock();
    }
}

從代碼中很顯然可以看出犁钟,因?yàn)楂@取不到鎖棱诱,因此線程阻塞在第83行,等待獲取鎖特纤,也就是說军俊,有別的線程持著這把鎖,但還沒釋放捧存,DubboShutdownHook不得不等待著

通過IDEA,查看有哪些地方獲取了這把鎖,如下昔穴,找到了org.apache.dubbo.registry.support.AbstractRegistryFactory#getRegistry(org.apache.dubbo.common.URL)會(huì)獲取鎖

// org.apache.dubbo.registry.support.AbstractRegistryFactory

public Registry getRegistry(URL url) {
    // ...(省略)
    LOCK.lock(); // 獲取鎖
    try {
        // ...(省略)
        // 創(chuàng)建Registry镰官,由于我們選用的注冊中心是Zookeeper,因此通過SPI選擇了ZookeeperRegistryFactory對ZookeeperRegistry進(jìn)行創(chuàng)建吗货,最終會(huì)調(diào)用到我們添加過一行System.exit的ZookeeperRegistry構(gòu)造函數(shù)中
        
        registry = createRegistry(url); 
        
        // ...(省略)
    } finally {
        // Release the lock
        LOCK.unlock(); // 創(chuàng)建完registry泳唠,與注冊中心連上之后,才會(huì)釋放鎖
    }
}
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory

public Registry createRegistry(URL url) {
    // 調(diào)用修改過源碼的ZookeeperRegistry構(gòu)造函數(shù)
    return new ZookeeperRegistry(url, zookeeperTransporter);
}

如此宙搬,System.exit無法退出JVM進(jìn)程的問題總算真相大白了:

  1. Dubbo啟動(dòng)過程中會(huì)先獲取鎖笨腥,然后創(chuàng)建registry與注冊中心進(jìn)行連接,在ZookeeperRegistry中調(diào)用了java.lang.System#exit方法勇垛,程序轉(zhuǎn)而執(zhí)行"喚起shutdown hook"的代碼并阻塞等待所有勾子函數(shù)執(zhí)行完畢脖母,而此時(shí),之前持有的鎖并沒有釋放
  2. 所有勾子函數(shù)(每個(gè)勾子函數(shù)都對應(yīng)一個(gè)線程)被喚醒并執(zhí)行闲孤,其中有一個(gè)Dubbo的勾子函數(shù)在執(zhí)行的過程中谆级,需要獲取步驟1中的鎖,由于獲取鎖失敗讼积,就阻塞等待著
  3. 由于1沒有釋放鎖的情況下等待2執(zhí)行完肥照,而2的執(zhí)行需要等待1釋放鎖,這樣就形成了一個(gè)類似"死鎖"的場景勤众,因此也就導(dǎo)致了程序卡死舆绎,而JVM進(jìn)程還存活的現(xiàn)象。之所以稱為"類似"死鎖们颜,是因?yàn)?中執(zhí)行System.exit的線程亿蒸,也即持有鎖的線程,永遠(yuǎn)不會(huì)走到釋放鎖的代碼:一旦程序進(jìn)入System.exit的世界里掌桩,就像進(jìn)了一個(gè)單向蟲洞边锁,只能進(jìn)不能出,如果勾子函數(shù)執(zhí)行完畢波岛,JVM進(jìn)程接著就會(huì)被關(guān)閉茅坛,不會(huì)有機(jī)會(huì)再釋放鎖

那么,為什么在異步線程中執(zhí)行System.exit则拷,卻能夠正常退出JVM贡蓖?

那是因?yàn)椋?strong>"喚起shutdown hook"并阻塞等待所有勾子函數(shù)執(zhí)行完畢的線程是其它線程(此處假設(shè)是線程A),該線程在阻塞時(shí)并未持有任何鎖煌茬,而主線程會(huì)繼續(xù)往下執(zhí)行并接著釋放鎖斥铺。一旦鎖釋放,Shutdown hook就有機(jī)會(huì)持有該鎖坛善,并且執(zhí)行其它資源的回收操作晾蜘,等到所有的shutdown hook執(zhí)行完畢邻眷,A線程就能從阻塞中返回并執(zhí)行halt方法關(guān)閉JVM,因此能夠正常退出JVM進(jìn)程

深入學(xué)習(xí)

以上是對java.lang.System#exit 無法退出程序問題的分析剔交,來龍去脈已經(jīng)闡述清楚肆饶,受益于對Dubbo源碼的了解以及正確的排查思路和排查手段,整個(gè)問題排查過程其實(shí)并沒有花太多時(shí)間岖常,但可以趁著這個(gè)機(jī)會(huì)驯镊,把java.lang.System#exit系統(tǒng)學(xué)習(xí)一下,或許會(huì)對以后問題排查竭鞍、基礎(chǔ)組件設(shè)計(jì)提供一些思路

System#exit
// java.lang.System

public static void exit(int status) {
    Runtime.getRuntime().exit(status);
}

Terminates the currently running Java Virtual Machine. The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination.
This method calls the exit method in class Runtime. This method never returns normally.
The call System.exit(n) is effectively equivalent to the call:
Runtime.getRuntime().exit(n)

這個(gè)方法實(shí)現(xiàn)非常簡單板惑,是Runtime#exit的一個(gè)簡便寫法,其作用是用來關(guān)閉JVM進(jìn)程偎快,一旦調(diào)用該方法冯乘,永遠(yuǎn)也不會(huì)從該方法正常返回:執(zhí)行完該方法后JVM進(jìn)程就直接關(guān)閉了。入?yún)tatus取值分兩類:0值與非0值滨砍,0值意味著正常關(guān)閉往湿,非0值意味著異常關(guān)閉。傳入0值[有可能]會(huì)去執(zhí)行所有的finalizer方法惋戏,非0值則一定不會(huì)執(zhí)行(都不正常了领追,還執(zhí)行啥finalizer呢?)。這兒提及[有可能]是因?yàn)橄旆辏J(rèn)并不會(huì)執(zhí)行finalizers绒窑,需要調(diào)用java.lang.Runtime#runFinalizersOnExit方法開啟,而該方法早被JDK標(biāo)識為Deprecated舔亭,因此通常情況下是不會(huì)開啟的

// java.lang.Runtime

@Deprecated
public static void runFinalizersOnExit(boolean value) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        try {
            security.checkExit(0);
        } catch (SecurityException e) {
            throw new SecurityException("runFinalizersOnExit");
        }
    }
    Shutdown.setRunFinalizersOnExit(value);
}

接著看java.lang.Runtime#exit些膨,可以看到,最終調(diào)用的是Shutdown.exit(status);钦铺,該方法是個(gè)包級別可見的方法订雾,外部不可見

// java.lang.Runtime

public void exit(int status) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkExit(status);
    }
    Shutdown.exit(status);
}
// java.lang.Shutdown

static void exit(int status) {
    // ...(省略)
    synchronized (Shutdown.class) {
        /* Synchronize on the class object, causing any other thread
         * that attempts to initiate shutdown to stall indefinitely
         */
        // 執(zhí)行shutdown序列
        sequence();
        // 關(guān)閉JVM
        halt(status);
    }
}
// java.lang.Shutdown

private static void sequence() {
    // ...(省略)
    runHooks();
    // ...(省略)
}
// java.lang.Shutdown

private static void runHooks() {
    for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
        try {
            Runnable hook;
            synchronized (lock) {
                // 這個(gè)鎖很重要,目的是通過Happens-Before保證內(nèi)存的可見性
                currentRunningHook = i;
                hook = hooks[i];
            }
            if (hook != null) hook.run(); //執(zhí)行勾子函數(shù)
        } catch(Throwable t) {
            if (t instanceof ThreadDeath) {
                ThreadDeath td = (ThreadDeath)t;
                throw td;
            }
        }
    }
}

java.lang.Shutdown#runHooks有兩個(gè)點(diǎn)需要注意矛洞,第一點(diǎn)MAX_SYSTEM_HOOKS(hooks)這個(gè)并不是我們注冊的shutdown hooks洼哎,而是按順序預(yù)定義的系統(tǒng)關(guān)閉勾子,目前JDK源碼(JDK8)預(yù)定義了三個(gè):

  • Console restore hook

  • Application hooks

  • DeleteOnExit hook

其中沼本,Application hooks才是我們應(yīng)用程序中主動(dòng)注冊的shutdown hook噩峦。在java.lang.ApplicationShutdownHooks類初始化時(shí),會(huì)執(zhí)行static代碼塊抽兆,并在其中注冊了Application hooks

// java.lang.ApplicationShutdownHooks

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    // 這個(gè)才是我們應(yīng)用程序代碼中注冊的shutdown hook
    private static IdentityHashMap<Thread, Thread> hooks; 
    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }

其次要注意的點(diǎn)是识补,給hook變量賦值的時(shí)候進(jìn)行了加鎖

Runnable hook;
synchronized (lock) {
    currentRunningHook = i;
    hook = hooks[i];
}

一般而言,給局部變量賦值是不需要加鎖的辫红,因?yàn)榫植孔兞渴菞I献兞科就浚€程棧之間數(shù)據(jù)是隔離的祝辣,不會(huì)出現(xiàn)線程安全的問題,因此不需要靠加鎖來保證數(shù)據(jù)并發(fā)訪問的安全性导盅。而此處加鎖也并非為了解決線程安全問題较幌,其真正的目的在于揍瑟,通過Happens-Before規(guī)則來保證hooks的內(nèi)存可見性:An unlock on a monitor happens-before every subsequent lock on that monitor白翻。如果不加鎖,有可能導(dǎo)致從hooks數(shù)組中讀取到的值并不是內(nèi)存中最新的變量值绢片,而是一個(gè)舊值

上面是讀取hooks數(shù)組給hook變量賦值滤馍,為了滿足HB(Happens-Before)原則,需要確保寫操作中同樣對hooks變量進(jìn)行了加鎖底循,因此我們看一下寫hooks數(shù)組的地方巢株,如下:

// java.lang.Shutdown

static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
    synchronized (lock) {
            // ...(省略)
        hooks[slot] = hook;
    }
}

操作確實(shí)加了鎖,這樣才能讓接下來的操作的加鎖行為滿足HB原則

由于篇幅原因熙涤,就不展開具體的HB介紹阁苞,相信了解過HB原則的朋友一下就能明白其中的原理

這個(gè)點(diǎn)個(gè)人感覺很有意思,因?yàn)殒i的作用不單是為了保證線程安全祠挫,還可以用來做為內(nèi)存通信那槽、保證內(nèi)存可見性的手段,因此可以當(dāng)作面試的一個(gè)點(diǎn)等舔,當(dāng)下次面試官問到:你寫的代碼中用過鎖(synchronized)嗎骚灸?什么場景用到鎖?都集群部署了慌植,單機(jī)鎖還有意義嗎甚牲? 我們就可以回答:為了保證內(nèi)存的可見性,balabalaba

所以你瞧蝶柿,這個(gè)點(diǎn)其實(shí)也給我們設(shè)計(jì)基礎(chǔ)組件帶來很大的啟發(fā)丈钙,synchronized在當(dāng)今集群、分布式環(huán)境下并非一無是處交汤,總有合適的地方在等待著它發(fā)揮光和熱

注:JDK源碼中真處處是寶藏雏赦,很多地方隱藏著巧妙而不可缺少的設(shè)計(jì)

在給hook變量賦值之后,就執(zhí)行 if (hook != null) hook.run();蜻展,其中會(huì)執(zhí)行到Application hooks喉誊,即上面提到的在ApplicationShutdownHooks類初始化時(shí)注冊的勾子,勾子內(nèi)部調(diào)用了java.lang.ApplicationShutdownHooks#runHooks方法

// java.lang.ApplicationShutdownHooks

Shutdown.add(1 /* shutdown hook invocation order */,
    false /* not registered if shutdown in progress */,
    new Runnable() {
        public void run() {
            runHooks();
        }
    }
);
// java.lang.ApplicationShutdownHooks

static void runHooks() {
    Collection<Thread> threads;
    synchronized(ApplicationShutdownHooks.class) {
        threads = hooks.keySet(); // hooks才是應(yīng)用程序真正注冊的shutdown hook
        hooks = null;
    }
        // 每一個(gè)shutdown hook都對應(yīng)一個(gè)thread纵顾,由此可見是并發(fā)執(zhí)行關(guān)閉勾子函數(shù)
    for (Thread hook : threads) {
        hook.start();
    }
    for (Thread hook : threads) {
        while (true) {
            try {
                hook.join(); // 死等到hook執(zhí)行完畢
                break;
            } catch (InterruptedException ignored) {
                // 即便被喚醒都不搭理伍茄,接著進(jìn)行下一輪循環(huán),繼續(xù)死等
            }
        }
    }
}

上面的hooks才是應(yīng)用程序真正注冊的shutdown hook施逾,由源碼可以看出敷矫,每一個(gè)hook都對應(yīng)著一個(gè)thread例获,且調(diào)用了它們的start方法,即開啟thread曹仗,意味著shutdown hook是并發(fā)榨汤、無序地執(zhí)行

接著,喚起shutdown hook的線程怎茫,會(huì)通過死循環(huán)和join死等到所有關(guān)閉勾子都執(zhí)行完畢收壕,且忽略任何喚醒異常。也即是說轨蛤,如果勾子們不執(zhí)行完蜜宪,喚醒線程是不會(huì)離開的

等所有的Application hooks執(zhí)行完畢,接下來會(huì)執(zhí)行DeleteOnExit hook(如果存在)祥山,等所有system hooks執(zhí)行完畢圃验,也基本意味著sequence方法執(zhí)行完畢,接下來就執(zhí)行halt方法關(guān)閉JVM虛擬機(jī)

synchronized (Shutdown.class) {
    sequence();
    halt(status);
}

這里額外還有一個(gè)知識點(diǎn)缝呕,上文只是提了一嘴澳窑,可能會(huì)容易忽略,此處拿出來解釋一下:執(zhí)行java.lang.System#exit永遠(yuǎn)也不會(huì)從該方法正常返回供常,也即是說摊聋,即便System#exit后邊跟著的是finally,也不會(huì)執(zhí)行 话侧。一不注意就容易掉坑里

try {
    // ...
    System.exit(0);
} finally {
    // 這里的代碼永遠(yuǎn)執(zhí)行不到
}
java.lang.Runtime#addShutdownHook

聊完System#exit方法栗精,接著來聊聊注冊shutdown hook的方法。該方法本身實(shí)現(xiàn)上很簡單瞻鹏,如下示:

// java.lang.Runtime
public void addShutdownHook(Thread hook) {
    // ...(省略)
    ApplicationShutdownHooks.add(hook);
}

// java.lang.ApplicationShutdownHooks
static synchronized void add(Thread hook) {
    // ...(省略)
    hooks.put(hook, hook);
}

需要注意的是悲立,注冊的關(guān)閉勾子會(huì)在以下幾種時(shí)機(jī)被調(diào)用到

  • 程序正常退出

    • 最后一個(gè)非守護(hù)線程執(zhí)行完畢退出時(shí)

    • System.exit方法被調(diào)用時(shí)

  • 程序響應(yīng)外部事件

    • 程序響應(yīng)用戶輸入事件,例如在控制臺按ctrl+c(^+c)

    • 程序響應(yīng)系統(tǒng)事件新博,如用戶注銷薪夕、系統(tǒng)關(guān)機(jī)等

除此之外,shutdown hook是不會(huì)被執(zhí)行的

Shutdown hook存在的意義之一赫悄,是能夠幫助我們實(shí)現(xiàn)優(yōu)雅停機(jī)原献,而優(yōu)雅停機(jī)的意義是:應(yīng)用的重啟、停機(jī)等操作埂淮,不影響業(yè)務(wù)的連續(xù)性

以Dubbo Provider的視角為例姑隅,優(yōu)雅停機(jī)需要滿足兩點(diǎn)基本訴求:

  1. Consumer不應(yīng)該請求到已經(jīng)下線的Provider
  2. 在途請求需要處理完畢,不能被停機(jī)指令中斷

Dubbo注冊了Shutdown hook倔撞,JVM在收到操作系統(tǒng)發(fā)來的關(guān)閉指令時(shí)讲仰,會(huì)執(zhí)行關(guān)閉勾子

  1. 在勾子中停止與注冊中心的連接,注冊中心會(huì)通知Consumer某個(gè)Provider已下線痪蝇,后續(xù)不應(yīng)該再調(diào)用該P(yáng)rovider進(jìn)行服務(wù)鄙陡。此行為是斷掉上游流量冕房,滿足第一點(diǎn)訴求

  2. 接著,勾子執(zhí)行Protocol(Dubbo相關(guān)概念)的注銷邏輯趁矾,在其中判斷server(Dubbo相關(guān)概念)是否還在處理請求耙册,在超時(shí)時(shí)間內(nèi)等待所有任務(wù)處理完畢,則關(guān)閉server毫捣。此行為是處理在途請求详拙,滿足第二點(diǎn)述求

因此,一種優(yōu)雅停機(jī)的整體方案如下:

$pid = ps | grep xxx // 查找要關(guān)閉的應(yīng)用
kill $pid // 發(fā)出關(guān)閉應(yīng)用指令
sleep for a period of time // 等待一段時(shí)間培漏,讓應(yīng)用程序執(zhí)行shutdown hook進(jìn)行現(xiàn)場的保留跟資源的清理工作

$pid = ps | grep xxx // 再次查找要關(guān)閉的應(yīng)用溪厘,如果還存在胡本,就需要強(qiáng)行關(guān)閉應(yīng)用
if($pid){kill -9 $pid} // 等待一段時(shí)間之后牌柄,應(yīng)用程序仍然沒有正常停止,則需要強(qiáng)行關(guān)閉應(yīng)用

總結(jié)

本文從現(xiàn)實(shí)問題出發(fā)侧甫,尋找了System#exit無法退出應(yīng)用程序的真相珊佣,這個(gè)過程中分享了一些排查問題思路跟手段;接著披粟,對System#exit進(jìn)行了整體深入的學(xué)習(xí)咒锻,閱讀了其核心部分的源碼,明白了關(guān)閉勾子其執(zhí)行過程跟原理守屉,其中還提到JDK巧妙的設(shè)計(jì):通過鎖來觸發(fā)Happens-Before規(guī)則惑艇,達(dá)到了內(nèi)存可見性的目的。再接著拇泛,分享了關(guān)閉勾子的注冊過程滨巴,了解了這些關(guān)閉勾子會(huì)被調(diào)用或觸發(fā)的時(shí)機(jī)。最后俺叭,闡述了關(guān)閉勾子的意義及重要性:優(yōu)雅停機(jī)恭取,并以Dubbo Provider進(jìn)行舉例,分享了Dubbo優(yōu)雅停機(jī)的原理熄守,加深對關(guān)閉勾子的理解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜈垮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子裕照,更是在濱河造成了極大的恐慌攒发,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晋南,死亡現(xiàn)場離奇詭異惠猿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)搬俊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門紊扬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜒茄,“玉大人,你說我怎么就攤上這事餐屎√锤穑” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵腹缩,是天一觀的道長屿聋。 經(jīng)常有香客問我,道長藏鹊,這世上最難降的妖魔是什么润讥? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮盘寡,結(jié)果婚禮上楚殿,老公的妹妹穿的比我還像新娘。我一直安慰自己竿痰,他們只是感情好脆粥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著影涉,像睡著了一般变隔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蟹倾,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天匣缘,我揣著相機(jī)與錄音,去河邊找鬼鲜棠。 笑死肌厨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岔留。 我是一名探鬼主播夏哭,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼献联!你這毒婦竟也來了竖配?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤里逆,失蹤者是張志新(化名)和其女友劉穎进胯,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體原押,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胁镐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盯漂。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颇玷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出就缆,到底是詐尸還是另有隱情帖渠,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布竭宰,位于F島的核電站空郊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏切揭。R本人自食惡果不足惜狞甚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望廓旬。 院中可真熱鬧哼审,春花似錦、人聲如沸嗤谚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巩步。三九已至,卻和暖如春桦踊,著一層夾襖步出監(jiān)牢的瞬間椅野,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工籍胯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留竟闪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓杖狼,卻偏偏與公主長得像炼蛤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蝶涩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345