您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java System#exit無法退出程序的問題如何解決”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java System#exit無法退出程序的問題如何解決”吧!
有朋友碰到了一個(gè)情況:java.lang.System#exit無法退出應(yīng)用程序。
我聽到這種情況的時(shí)候是感覺很驚奇的,這函數(shù)還能不起作用?這就好奇不已了呀
接著,朋友繼續(xù)給出了他的場景描述:在Dubbo應(yīng)用連接注冊(cè)中心的時(shí)候,如果連接(超時(shí))失敗,期望調(diào)用System#exit退出應(yīng)用程序,但是程序并沒有按期望退出,JVM進(jìn)程還存在
與此同時(shí),如果把執(zhí)行System#exit的代碼放到另一個(gè)線程,程序可以按期望退出,JVM進(jìn)程結(jié)束
用偽代碼描述如下:
Future<Object> future = 連接注冊(cè)中心的Future; try { Object o = future.get(3, TimeUnit.SECONDS); } catch (Exception e) { log.error("connect failed xxxx"); System.exit(1); // 程序無法退出 } ----------- Future<Object> future = 連接注冊(cè)中心的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ù)備知識(shí),否則會(huì)茫然無措,感覺無從下手
java.lang.System#exit 方法是Java提供的能夠停止JVM進(jìn)程的方法
該方法被觸發(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方法里先注冊(cè)了一個(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é)果是控制臺(tái)不斷打印"closing…",且JVM進(jìn)程沒有退出
原因正是上述第二點(diǎn)儲(chǔ)備知識(shí)提到的:JVM會(huì)等待所有勾子執(zhí)行完畢之后,才關(guān)閉進(jìn)程。而示例中的shutdown hook 永遠(yuǎn)也不會(huì)執(zhí)行完畢,因此JVM進(jìn)程也不會(huì)被關(guān)閉
盡管有了儲(chǔ)備知識(shí),仍然很疑惑:如果存在死循環(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)程不能退出
恰好對(duì)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ì)去注冊(cè)中心(這兒是Zookeeper)注冊(cè)或者訂閱,因?yàn)閱?dòng)的是消費(fèi)者,因此應(yīng)用會(huì)嘗試連接注冊(cè)中心Zookeeper,會(huì)走到ZookeeperRegistry
的構(gòu)造函數(shù),由于構(gòu)造函數(shù)第二行是新增的代碼System.exit(1);
按照背景的說法,JVM不會(huì)退出,且會(huì)卡死,這時(shí)候,借助IDEA的"快照"功能,可以"拍"下Java線程棧的運(yùn)行情況,功能上相當(dāng)于執(zhí)行jstack
命令
從線程棧中看出一個(gè)可疑的線程:DubboShutdownHook
從名字上可以看出是一個(gè)Dubbo注冊(cè)的一個(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,由于我們選用的注冊(cè)中心是Zookeeper,因此通過SPI選擇了ZookeeperRegistryFactory對(duì)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,與注冊(cè)中心連上之后,才會(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與注冊(cè)中心進(jìn)行連接,在ZookeeperRegistry中調(diào)用了java.lang.System#exit方法,程序轉(zhuǎn)而執(zhí)行"喚起shutdown hook"的代碼并阻塞等待所有勾子函數(shù)執(zhí)行完畢,而此時(shí),之前持有的鎖并沒有釋放
2.所有勾子函數(shù)(每個(gè)勾子函數(shù)都對(duì)應(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í)
以上是對(duì)java.lang.System#exit 無法退出程序問題的分析,來龍去脈已經(jīng)闡述清楚,受益于對(duì)Dubbo源碼的了解以及正確的排查思路和排查手段,整個(gè)問題排查過程其實(shí)并沒有花太多時(shí)間,但可以趁著這個(gè)機(jī)會(huì),把java.lang.System#exit
系統(tǒng)學(xué)習(xí)一下,或許會(huì)對(duì)以后問題排查、基礎(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)?,默認(rèn)并不會(huì)執(zhí)行finalizers,需要調(diào)用java.lang.Runtime#runFinalizersOnExit
方法開啟,而該方法早被JDK標(biāo)識(shí)為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è)包級(jí)別可見的方法,外部不可見
// 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è)并不是我們注冊(cè)的shutdown hooks,而是按順序預(yù)定義的系統(tǒng)關(guān)閉勾子,目前JDK源碼(JDK8)預(yù)定義了三個(gè):
Console restore hook
Application hooks
DeleteOnExit hook
其中,Application hooks才是我們應(yīng)用程序中主動(dòng)注冊(cè)的shutdown hook。
在java.lang.ApplicationShutdownHooks
類初始化時(shí),會(huì)執(zhí)行static代碼塊,并在其中注冊(cè)了Application hooks
// java.lang.ApplicationShutdownHooks class ApplicationShutdownHooks { /* The set of registered hooks */ // 這個(gè)才是我們應(yīng)用程序代碼中注冊(cè)的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)
原則,需要確保寫操作中同樣對(duì)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í)也給我們?cè)O(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í)注冊(cè)的勾子,勾子內(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)用程序真正注冊(cè)的shutdown hook hooks = null; } // 每一個(gè)shutdown hook都對(duì)應(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)用程序真正注冊(cè)的shutdown hook,由源碼可以看出,每一個(gè)hook都對(duì)應(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è)知識(shí)點(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
方法,接著來聊聊注冊(cè)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); }
需要注意的是,注冊(cè)的關(guān)閉勾子會(huì)在以下幾種時(shí)機(jī)被調(diào)用到
程序正常退出
最后一個(gè)非守護(hù)線程執(zhí)行完畢退出時(shí)
System.exit方法被調(diào)用時(shí)
程序響應(yīng)外部事件
程序響應(yīng)用戶輸入事件,例如在控制臺(tái)按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)基本訴求:
Consumer不應(yīng)該請(qǐng)求到已經(jīng)下線的Provider
在途請(qǐng)求需要處理完畢,不能被停機(jī)指令中斷
Dubbo注冊(cè)了Shutdown hook,JVM在收到操作系統(tǒng)發(fā)來的關(guān)閉指令時(shí),會(huì)執(zhí)行關(guān)閉勾子
在勾子中停止與注冊(cè)中心的連接,注冊(cè)中心會(huì)通知Consumer某個(gè)Provider已下線,后續(xù)不應(yīng)該再調(diào)用該P(yáng)rovider進(jìn)行服務(wù)。此行為是斷掉上游流量,滿足第一點(diǎn)訴求
接著,勾子執(zhí)行Protocol(Dubbo相關(guān)概念)的注銷邏輯,在其中判斷server(Dubbo相關(guān)概念)是否還在處理請(qǐng)求,在超時(shí)時(shí)間內(nèi)等待所有任務(wù)處理完畢,則關(guān)閉server。此行為是處理在途請(qǐng)求,滿足第二點(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)用
感謝各位的閱讀,以上就是“Java System#exit無法退出程序的問題如何解決”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)Java System#exit無法退出程序的問題如何解決這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。