- 1. 分代收集理論
- 2. 垃圾收集算法
- 3. 并發(fā)的可達性分析
1. 分代收集理論
當(dāng)前商業(yè)虛擬機的垃圾收集器,大多數(shù)都遵循了“分代收集”(Generational Collection)的理論進行設(shè)計俭正。它建立在兩個分代假說之上:
- 弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的歌殃。
- 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。
對堆劃分區(qū)域,將回收對象依據(jù)其年齡分配到不同的區(qū)域
這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設(shè)計原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域赚窃,然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲乖坠。
如果一個區(qū)域中大多數(shù)對象都是朝生夕滅,難以熬過垃圾收集過程的話乏屯,那么把它們集中放在一起,每次回收時只關(guān)注如何保留少量存活而不是去標記那些大量將要被回收的對象瘦赫,就能以較低代價回收到大量的空間辰晕。
如果剩下的都是難以消亡的對象,那把它們集中放在一塊确虱,虛擬機便可以使用較低的頻率來回收這個區(qū)域含友,這就同時兼顧了垃圾收集的時間開銷和內(nèi)存的空間有效利用。
堆劃分
所以堆分為為新生代(Young Generation)和老年代(Old Generation)兩個區(qū)域校辩。
在新生代中窘问,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,剩下的逐步放到老年代宜咒。
跨代引用
新生代中的對象是完全有可能被老年代所引用的惠赫,為了找出該區(qū)域中的存活對象,不得不在固定的GC Roots之外故黑,再額外遍歷整個老年代中所有對象來確倍郏可達性分析結(jié)果的正確性,反過來也是一樣。這樣無疑會為內(nèi)存回收帶來很大的性能負擔(dān)场晶。
解決理論
- 跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對于同代引用來說僅占極少數(shù)混埠。
存在互相引用關(guān)系的兩個對象,是應(yīng)該傾向于同時生存或者同時消亡的诗轻。
存在互相引用的兩個對象應(yīng)該同時生存或者同時消亡钳宪。所以存在跨代引用的新生代對象會因為老年代對象難以消亡,也 得意存活 , 最后也晉升到老年代, 跨代引用被消除。
記憶集
在新生代建立一個記憶集, 將老年代標識為一小塊的區(qū)域,標識哪些區(qū)域存在跨代引用扳炬。當(dāng)發(fā)生MinorGC時,掃描GC Roots + 記憶集中 存在 跨代引用的老年代區(qū)域?qū)ο笞鳛镚C 也作為GC Roots進行掃描吏颖。
雖然這種方法需要在對象改變引用關(guān)系(如將自己或者某個屬性賦值)時維護記錄數(shù)據(jù)的正確性,會增加一些運行時的開銷鞠柄,但比起收集時掃描整個老年代來說仍然是劃算的侦高。
垃圾回收分類
1. 部分收集(Partial GC)
1.1 新生代收集(Minor GC/Young GC):
指目標只是新生代的垃圾收集。
1.2 老年代收集(Major GC/Old GC):
指目標只是老年代的垃圾收集厌杜。目前只有CMS收集器會有單獨收集老年代的行為奉呛。
1.3 混合收集(Mixed GC)
指目標是收集整個新生代以及部分老年代的垃圾收集计螺。目前只有G1收集器會有這種行為。
2. 整堆收集(Full GC)
收集整個Java堆和方法區(qū)的垃圾收集瞧壮。
2. 垃圾收集算法
2.1 標記-清除算法
如它的名字一樣登馒,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后咆槽,統(tǒng)一回收掉所有被標記的對象陈轿,也可以反過來,標記存活的對象秦忿,統(tǒng)一回收所有未被標記的對象麦射。
標記過程就是判定對象是否屬于垃圾的過程。
缺點
- 效率不穩(wěn)定灯谣,當(dāng)需要回收的對象過多時潜秋,大量標記和清除的動作會導(dǎo)致導(dǎo)致標記和清除兩個過程的執(zhí)行效率都隨對象數(shù)量增長而降低
- 內(nèi)存空間的碎片化問題,標記胎许、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片峻呛,內(nèi)存碎片太多可能會導(dǎo)致 大對象無法找到足夠的連續(xù)內(nèi)存 而不得不 提前觸發(fā)GC。
2.2 標記-復(fù)制算法
標記-復(fù)制算法常被簡稱為復(fù)制算法辜窑。為了解決標記-清除算法面對大量可回收對象時執(zhí)行效率低的問題钩述,該算法將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊穆碎。當(dāng)這一塊的內(nèi)存用完了牙勘,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉惨远。
優(yōu)點
實現(xiàn)簡單谜悟,運行高效。如果多數(shù)對象都是可回收的情況北秽,算法需要復(fù)制的就是占少數(shù)的存活對象,而且每次都是針對整個半?yún)^(qū)進行內(nèi)存回收最筒,分配內(nèi)存時也就不用考慮有空間碎片的復(fù)雜情況贺氓,只要移動堆頂指針,按順序分配即可床蜘。
缺點
- 如果內(nèi)存中多數(shù)對象都是存活的辙培,這種算法將會產(chǎn)生大量的內(nèi)存間復(fù)制的開銷。
- 內(nèi)存利用率只有 一半邢锯。
2.2.1. 應(yīng)用場景 : 新生代
新生代中的對象有98%熬不過第一輪收集扬蕊。因此并不需要按照1∶1的比例來劃分新生代的內(nèi)存空間。
針對具備“朝生夕滅”特點的對象,產(chǎn)生了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略丹擎,現(xiàn)在稱為“Appel式回收”尾抑。
From:To:Eden=1:1:8
HotSpot虛擬機的Serial歇父、ParNew等新生代收集器均采用了這種策略來設(shè)計新生代的內(nèi)存布局。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間(From區(qū)和To區(qū))再愈,每次分配內(nèi)存只使用Eden和其中一塊Survivor榜苫。發(fā)生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性復(fù)制到另外一塊Survivor空間上翎冲,然后直接清理掉Eden和已用過的那塊Survivor空間垂睬。
HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%)抗悍,只有一個Survivor空間驹饺,即10%的新生代是會被“浪費”的。
分配擔(dān)保
然缴渊,98%的對象可被回收僅僅是“普通場景”下測得的數(shù)據(jù)赏壹,任何人都沒有辦法百分百保證每次回收都只有不多于10%的對象存活。當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對象時疟暖,就需要依賴其他內(nèi)存區(qū)域(實際上大多就是老年代)進行分配擔(dān)保(Handle Promotion)卡儒。
2.3 標記-整理算法
標記-復(fù)制算法在對象存活率較高時就要進行較多的復(fù)制操作,效率將會降低俐巴。更關(guān)鍵的是骨望,如果不想浪費50%的空間(Apple式回收改進的標記復(fù)制算法, 1:1:8 ),就需要有額外的空間進行分配擔(dān)保欣舵,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況擎鸠,所以在老年代一般不能直接選用這種算法。
針對老年代對象的存亡特征缘圈,標記-整理”(Mark-Compact)算法,其中的標記過程仍然與“標記-清除”算法一樣劣光,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動糟把,然后直接清理掉邊界以外的內(nèi)存绢涡。
吞吐量和GC延遲的考量
吞吐量的實質(zhì)是賦值器(Mutator,可以理解為使用垃圾收集的用戶程序遣疯,本書為便于理解雄可,多數(shù)地方用“用戶程序”或“用戶線程”代替)與收集器的效率總和。
Stop The World(會產(chǎn)生GC延遲)
如果移動存活對象缠犀,尤其是在老年代這種每次回收都有大量對象存活區(qū)域数苫,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應(yīng)用程序才能進行辨液,像這樣的停頓被描述為“Stop The World”虐急。
但如果跟標記-清除算法那樣完全不考慮移動和整理存活對象的話√下酰空間碎片化問題只能就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來解決止吁。內(nèi)存的訪問是用戶程序最頻繁的操作被辑,假如在這個環(huán)節(jié)上增加了額外的負擔(dān),勢必會直接影響應(yīng)用程序的吞吐量赏殃。
從垃圾收集的停頓時間來看敷待,不移動對象停頓時間會更短,甚至可以不需要停頓仁热,但是從整個程序的吞吐量來看榜揖,移動對象會更劃算。
吞吐量的實質(zhì)是賦值器(Mutator抗蠢,可以理解為使用垃圾收集的用戶程序举哟,本書為便于理解,多數(shù)地方用“用戶程序”或“用戶線程”代替)與收集器的效率總和迅矛。即使不移動對象會使得收集器的效率提升一些妨猩,但因內(nèi)存分配和訪問相比垃圾收集頻率要高得多,這部分的耗時增加秽褒,總吞吐量仍然是下降的壶硅。
HotSpot虛擬機里面關(guān)注吞吐量的Parallel Scavenge收集器是基于標記-整理算法的。
而關(guān)注延遲的CMS收集器則是基于標記-清除算法的:為了不在內(nèi)存分配和訪問上增加太大額外負擔(dān)销斟,做法是讓虛擬機平時多數(shù)時間都采用標記-清除算法庐椒,暫時容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng)大到影響對象分配時蚂踊,再采用標記-整理算法收集一次约谈,以獲得規(guī)整的內(nèi)存空間。
2.4 HotSpot的算法細節(jié)實現(xiàn)
2.4.1. 根節(jié)點枚舉
從全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中 固定可作為GC Roots的節(jié)點犁钟。
迄今為止棱诱,所有收集器在根節(jié)點枚舉這一步驟時都是必須暫停用戶線程的,就是“Stop The World”±远可達性分析算法耗時最長的查找引用鏈的過程已經(jīng)可以做到與用戶線程一起并發(fā),但根節(jié)點枚舉始終還是必須在一個能保障一致性的快照中才得以進行迈勋。這里“一致性”的意思是整個枚舉期間執(zhí)行子系統(tǒng)看起來就像被凍結(jié)在某個時間點上,不會出現(xiàn)分析過程中醋粟,根節(jié)點集合的對象引用關(guān)系還在不斷變化的情況粪躬,
OopMap
當(dāng)用戶線程停頓下來之后,為了避免需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位,虛擬機應(yīng)當(dāng)是有辦法直接得到哪些地方存放著對象引用的。
在HotSpot的解決方案里昔穴,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達到這個目的。一旦類加載動作完成的時候提前,HotSpot就會把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來吗货,這樣收集器在掃描時就可以直接得知這些信息了,并不需要真正一個不漏地從方法區(qū)等GC Roots開始查找狈网。
2.4.2. 安全點
HotSpot沒有為每條指令都生成OopMap宙搬,因為導(dǎo)致OopMap內(nèi)容變化的指令非常多笨腥,如果為每一條指令都生成對應(yīng)的OopMap,那將會需要大量的額外存儲空間勇垛,這樣垃圾收集的成本太高脖母。
只是在“特定的位置”記錄了這些信息,這些位置被稱為安全點(Safepoint)闲孤。決定了用戶程序執(zhí)行時并非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集谆级,而是強制要求必須執(zhí)行到達安全點后才能夠暫停。
2.4.2.1. 安全點的選定
安全點位置的選取基本上是以“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的讼积。
“長時間執(zhí)行”的最明顯特征就是指令序列的復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)攒岛、異常跳轉(zhuǎn)等都屬于指令序列復(fù)用花吟,所以只有具有這些功能的指令才會產(chǎn)生安全點。
2.4.2.2. GC時,如何讓線程跑到安全點停頓
搶先式中斷(Preemptive Suspension)
搶先式中斷不需要線程的執(zhí)行代碼主動去配合们颜,在垃圾收集發(fā)生時吕朵,系統(tǒng)首先把所有用戶線程全部中斷,如果發(fā)現(xiàn)有用戶線程中斷的地方不在安全點上窥突,就恢復(fù)這條線程執(zhí)行努溃,讓它一會再重新中斷,直到跑到安全點上波岛。
現(xiàn)在幾乎沒有虛擬機實現(xiàn)采用搶先式中斷來暫停線程響應(yīng)GC事件茅坛。
主動式中斷(Voluntary Suspension)
主動式中斷的思想是當(dāng)垃圾收集需要中斷線程的時候,不直接對線程操作则拷,僅僅簡單地設(shè)置一個標志位贡蓖,各個線程執(zhí)行過程時會不停地主動去輪詢這個標志,一旦發(fā)現(xiàn)中斷標志為真時就自己在最近的安全點上主動中斷掛起煌茬。
由于輪詢操作在代碼中會頻繁出現(xiàn)斥铺,這要求它必須足夠高效。HotSpot使用內(nèi)存保護陷阱的方式坛善,把輪詢操作精簡至只有一條匯編指令的程度晾蜘。當(dāng)需要暫停用戶線程時,虛擬機把0x160100的內(nèi)存頁設(shè)置為不可讀眠屎,那線程執(zhí)行到test指令時就會產(chǎn)生一個自陷異常信號剔交,然后在預(yù)先注冊的異常處理器中掛起線程實現(xiàn)等待,這樣僅通過一條匯編指令便完成安全點輪詢和觸發(fā)線程中斷了改衩。
2.4.3. 安全區(qū)域
如果GC的時候,線程并沒有分到cpu時間片岖常,也就是沒有在執(zhí)行,就無法響應(yīng)jvm的中斷請 求 (比如線程處于 sleep,blocked狀態(tài)),不能跑到安全點掛起自己。
虛擬機也顯然不可能持續(xù)等待線程重新被激活分配處理器時間葫督。對于這種情況竭鞍,就必須引入安全區(qū)域(Safe Region)來解決板惑。
安全區(qū)域是指能夠確保在某一段代碼片段之中,引用關(guān)系不會發(fā)生變化偎快,因此冯乘,在這個區(qū)域中任意地方開始垃圾收集都是安全的。我們也可以把安全區(qū)域看作被擴展拉伸了的安全點晒夹。
當(dāng)用戶線程執(zhí)行到安全區(qū)域里面的代碼時裆馒,首先會標識自己已經(jīng)進入了安全區(qū)域,那樣當(dāng)這段時間里虛擬機要發(fā)起垃圾收集時就不必去管這些已聲明自己在安全區(qū)域內(nèi)的線程了惋戏。當(dāng)線程要離開安全區(qū)域時领追,它要檢查虛擬機是否已經(jīng)完成了根節(jié)點枚舉(或者垃圾收集過程中其他需要暫停用戶線程的階段),如果完成了响逢,那線程就當(dāng)作沒事發(fā)生過绒窑,繼續(xù)執(zhí)行;否則它就必須一直等待舔亭,直到收到可以離開安全區(qū)域的信號為止些膨。
2.5 記憶集與卡表
為解決對象跨代引用所帶來的問題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數(shù)據(jù)結(jié)構(gòu)钦铺,用以避免把整個老年代加進GC Roots掃描范圍订雾。
不只新生代,老年代才有跨代引用的問題,所有 部分區(qū)域行為(Partial GC)的垃圾收集器(如G1、ZGC和Shenandoah),都有跨代引用的問題矛洞。
記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)洼哎。
收集器只需要通過記憶集判斷出某一塊非收集區(qū)域是否存在有指向了收集區(qū)域的指針就可以了,并不需要了解這些跨代指針的全部細節(jié)沼本。
2.5.1 可供選擇(當(dāng)然也可以選擇這個范圍以外的)的記錄精度:
1. 字長精度
每個記錄精確到一個機器字長(就是處理器的尋址位數(shù)噩峦,如常見的32位或64位,這個精度決定了機器訪問物理內(nèi)存地址的指針長度)抽兆,該字包含跨代指針识补。
2. 對象精度
每個記錄精確到一個對象,該對象里有字段含有跨代指針辫红。
3. 卡精度
每個記錄精確到一塊內(nèi)存區(qū)域凭涂,該區(qū)域內(nèi)有對象含有跨代指針。
卡表
“卡表”(Card Table)贴妻,是目前最常用的一種記憶集實現(xiàn)形式,是記憶集的一種實現(xiàn)切油。定義了記憶集的記錄精度、與堆內(nèi)存的映射關(guān)系等名惩。
hotspot卡表實現(xiàn)
CARD_TABLE [this address >> 9] = 0;
字節(jié)數(shù)組CARD_TABLE的每一個元素都對應(yīng)著其標識的內(nèi)存區(qū)域中一塊特定大小的內(nèi)存塊白翻,這個內(nèi)存塊被稱作“卡頁”,卡頁大小為 2的9次方。即512字節(jié)(地址右移9位,相當(dāng)于用地址除以512)滤馍。
一個卡頁的內(nèi)存中通常包含不止一個對象,只要卡頁內(nèi)有一個(或更多)對象的字段存在著跨代指針底循,那就將對應(yīng)卡表的數(shù)組元素的值標識為1巢株,稱為這個元素變臟(Dirty),沒有則標識為0熙涤。在垃圾收集發(fā)生時阁苞,只要篩選出卡表中變臟的元素,就能輕易得出哪些卡頁內(nèi)存塊中包含跨代指針祠挫,把它們加入GC Roots中一并掃描那槽。
2.5.2 寫屏障
在HotSpot虛擬機里是通過寫屏障(Write Barrier)技術(shù)維護卡表狀態(tài)的。寫屏障可以看作在虛擬機層面對“引用類型字段賦值”這個動作的AOP切面[[2]](javascript:void(0);)等舔,在引用對象賦值時會產(chǎn)生一個環(huán)形(Around)通知骚灸,供程序執(zhí)行額外的動作,也就是說賦值的前后都在寫屏障的覆蓋范疇內(nèi)慌植。在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier)甚牲,在賦值后的則叫作寫后屏障(Post-Write Barrier)。
void oop_field_store(oop* field, oop new_value) {
// 引用字段賦值操作
*field = new_value;
// 寫后屏障蝶柿,在這里完成卡表狀態(tài)更新
post_write_barrier(field, new_value);
}
應(yīng)用寫屏障后丈钙,虛擬機就會為所有賦值操作生成相應(yīng)的指令,一旦收集器在寫屏障中增加了更新卡表操作交汤,無論更新的是不是老年代對新生代對象的引用雏赦,每次只要對引用進行更新,就會產(chǎn)生額外的開銷芙扎,不過這個開銷與Minor GC時掃描整個老年代的代價相比還是低得多的星岗。
偽共享問題
除了寫屏障的開銷外,卡表在高并發(fā)場景下還面臨著“偽共享”(False Sharing)問題∽莨耍現(xiàn)代中央處理器的緩存系統(tǒng)中是以緩存行(Cache Line)為單位存儲的伍茄,當(dāng)多線程修改互相獨立的變量時,如果這些變量恰好共享同一個緩存行施逾,就會彼此影響(寫回敷矫、無效化或者同步)而導(dǎo)致性能降低,這就是偽共享問題汉额。
假設(shè)處理器的緩存行大小為64字節(jié)曹仗,由于一個卡表元素占1個字節(jié),64個卡表元素將共享同一個緩存行蠕搜。這64個卡表元素對應(yīng)的卡頁總的內(nèi)存為32KB(64×512字節(jié))怎茫,也就是說如果不同線程更新的對象正好處于這32KB的內(nèi)存區(qū)域內(nèi),就會導(dǎo)致更新卡表時正好寫入同一個緩存行而影響性能。
解決方式
不采用無條件的寫屏障轨蛤,而是先檢查卡表標記蜜宪,只有當(dāng)該卡表元素未被標記過時才將其標記為變臟。
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
在JDK 7之后祥山,HotSpot虛擬機增加了一個新的參數(shù)-XX:+UseCondCardMark圃验,用來決定是否開啟卡表更新的條件判斷。
3. 并發(fā)的可達性分析
三色標記法
白色:表示對象尚未被垃圾收集器訪問過缝呕。顯然在可達性分析剛剛開始的階段澳窑,所有的對象都是白色的,若在分析結(jié)束的階段供常,仍然是白色的對象摊聋,即代表不可達。
黑色:表示對象已經(jīng)被垃圾收集器訪問過栈暇,且這個對象的所有引用都已經(jīng)掃描過麻裁。黑色的對象代表已經(jīng)掃描過,它是安全存活的瞻鹏,如果有其他對象引用指向了黑色對象悲立,無須重新掃描一遍。黑色對象不可能直接(不經(jīng)過灰色對象)指向某個白色對象新博。
灰色:表示對象已經(jīng)被垃圾收集器訪問過薪夕,但這個對象上至少存在一個引用還沒有被掃描過。
當(dāng)且僅當(dāng)以下兩個條件同時滿足時赫悄,會產(chǎn)生“對象消失”的問題原献,即原本應(yīng)該是黑色的對象被誤標為白色:
賦值器插入了一條或多條從黑色對象到白色對象的新引用;
賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用埂淮。
因此姑隅,我們要解決并發(fā)掃描時的對象消失問題,只需破壞這兩個條件的任意一個即可倔撞。
1. 增量更新(Incremental Update)
當(dāng)黑色對象插入新的指向白色對象的引用關(guān)系時讲仰,就將這個新插入的引用記錄下來,等并發(fā)掃描結(jié)束之后痪蝇,再將這些記錄過的引用關(guān)系中的黑色對象為根鄙陡,重新掃描一次。這可以簡化理解為躏啰,黑色對象一旦新插入了指向白色對象的引用之后趁矾,它就變回灰色對象了。
2. 原始快照
當(dāng)灰色對象要刪除指向白色對象的引用關(guān)系時给僵,就將這個要刪除的引用記錄下來毫捣,在并發(fā)掃描結(jié)束之后,再將這些記錄過的引用關(guān)系中的灰色對象為根,重新掃描一次蔓同。這也可以簡化理解為饶辙,無論引用關(guān)系刪除與否,都會按照剛剛開始掃描那一刻的對象圖快照來進行搜索牌柄。
以上無論是對引用關(guān)系記錄的插入還是刪除畸悬,虛擬機的記錄操作都是通過寫屏障實現(xiàn)的。在HotSpot虛擬機中珊佣,增量更新和原始快照這兩種解決方案都有實際應(yīng)用,譬如披粟,CMS是基于增量更新來做并發(fā)標記的咒锻,G1、Shenandoah則是用原始快照來實現(xiàn)守屉。