一 G1收集器
- g1收集器是一個面向服務端的垃圾收集器適用於多核處理器、大內存容量的服務端系統。
- 它滿足短時間gc停頓的同時達到一個較高的吞吐量。
- JDK7以上版本適用
先介紹兩個概念:吞吐量和響應能力,響應能力和吞吐量是評價一個系統的兩個重要指標
吞吐量
- 吞吐量關注的是,在一個指定的時間內,最大化一個應用的工作量。
- 如下方式來衡量一個系統吞吐量的好壞:
- 在一定時間內同一個事務(或者任務、請求)完成的次數(tps)
- 數據庫一定時間以完成多少次查詢
- 對於關注吞吐量的系統,一定次數卡頓(即stw)是可以接受的,因為這個系統關注長時間的大量任務的執行能力,單次快速的響應並不值得考慮
響應能力
響應能力指一個程序或者系統對請求是否能夠及時響應,比如:
- 一個桌面UI能多快地響應一個事件
- 一個網站能夠多快返回一個頁面請求
- 數據庫能夠多快返回查詢的數據
對於這類對響應能力敏感的場景,長時間的停頓是無法接受的,關注每一次反應的能力。
1.1 G1收集器的設計目標
與應用線程同時工作,幾乎不需要 stop the work(與CMS類似);
-
G1的設計規划是要替換掉CMS,G1在某些方面彌補了CMS的不足,比如CMS使用的是mark-sweep算法,自然會產生內存碎片(CMS只能在Full GC時,用 stop the world整理內存碎片),然而G1基於copying算法,高效的整理剩余內存,而不需要管理內存碎片
-
GC停頓更加可控,甚至可以設置一個時間閾值,比如說可以回收某些部分的老年代,而不像CMS老年代必須全部回收(其它收集器時間可能難以把握)
1.2 G1重要概念
1.2.1 分區( Region)
-
G1采取了不同的策略來解決並行、串行和CMS收集器的碎片、暫停時間不可控等問題—G1將整個堆分成相同大小的分區或稱為區域( Region)
- G1的堆結構如下:
- 每個分區都可能是年輕代也可能是老年代,但是在同一時刻只能屬於某個代。年輕代,survivor區,老年代這些概念還存在,成為邏輯上的概念,這樣方便復用之前分代框架的邏輯。分區在物理上不需要連續,則帶來了額外的好處,可以在老年代中只對某些區域(Region)進行回收。對於新生代依然是在新生代滿了的時候,對整個新生代進行回收一一整個新生代中的對象,要么被回收、要么晉升,至於新生代也采取分區機制的原因,則是因為這樣跟老年代的策略統一,方便調整代的大小【其實新生代並不適合這樣的垃圾收集,因為是對新生代的所有區域進行回收!不用像老年代那樣選擇回收效益高的Region】
- 在G1中,還有一種特殊的區域,叫 Humongous區域。如果一個對象占用的空間達到或是超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在老年代,但是如果它是一個短期存在的巨型對象就會對垃圾收集器造成負面影響。為了解決這個問題,G1划分了一個 Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC
- G1的堆結構如下:
1.2.2 收集集合(CSet)
收集集合(Collection Set):一組可被回收的分區的集合,在CSet中存活的數據會在GC過程中被移動到另一個可用分區,CSet中的分區可以來自eden空間、 survivor空間、或者老年代,並且可以同時包含這幾代分區的內容。
1.2.3 已記憶集合(RSet)
已記憶集合(Remember Set):RSet使區域Region的並行和獨立成為可能,RSet記錄了其他Region中的對象引用本 Region中對象的關系,屬於 points-into結構(誰引用了我的對象)。RSet的價值在於使得垃圾收集器不需要掃描整個堆找到誰引用了當前分區中的對象,只需要掃描RSet即可。
point-into Rset解析
G1 GC是在 points-out的 card table之上再加了一層結構來構成 points-into Rset:每個Region會使用point-into Rset記錄下到底哪些別的Region有指向自己的指針,而這些指針分別在哪些card的范圍內【也就是說 card table 記錄自己指向了誰,Point-into Ret 記錄了誰指向了自己】。這個RSet其實是一個 hash table,key是別的Region的起始地址,value是一個集合,里面的元素是 card table的 Index.舉例來說,如果region A的Rset里有一項的key是 region B,value里有 index為1234的card,它的意思就是 region B的一個card里有引用指向region A。所以對 region A來說該RSet記錄的是 points-into的關系,而 card table仍然記錄了 points-out的關系。
car table 解析
card table是什么又是怎么來的呢?如果一個對象引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個問題,在G1中又引入了另外一個概念,卡表( Card Table)。一個 Card Table將一個分區在邏輯上划分為固定大小的連續區域每個區域稱之為卡。卡通常較小,介於128到512字節之間。 Card Table通常為字節數組,由Card的索引(即數組下標)來標識每個分區的空間地址
下圖表示了RSet、Card和Region的關系):
上圖中有三個Region,每個Region被分成了多個Card,在不同Region中的Card會相互引用,Region1中的Card中的對象引用了Region2中的Card中的對象,藍色實線表示的就是points-out的關系,而在Region2的RSet中,記錄了Region1的Card,即紅色虛線表示的關系,這就是points- into。
而維系RSet中的引用關系靠post-write barrier和Concurrent refinement threads來維護,操作偽代碼如下:
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
*field = new_value; // the actual store
post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}
RSet究竟是怎么輔助GC的呢?在做YGC的時候,只需要選定young generation region的RSet作為根集,這些RSet記錄了old->young的跨代引用,避免了掃描整個old generation。 而mixed gc的時候,old generation中記錄了old->old的RSet,young->old的引用由掃描全部young generation region得到,這樣也不用掃描全部old generation region。所以RSet的引入大大減少了GC的工作量。
1.2.4 SATB
Snapshot-at-the-beginning(SATB)SATB是G1 GC在全局並發標記階段使用的增量式的標記算法。並發標記是並發多線程的,但並發線程在同一時刻只掃描一個分區
1.3 GC模式
G1提供了兩種GC模式, Young GC和 Mixed Gc兩種都是完全 Stop The World的
1.3.1 Young GC
Young GC:將選擇所有年輕代里的 Region進行GC(所以CSet里存的就是所有年輕代里面的Region)。將存活的對象復制到幸survivor區是多線程執行的並且需要stw,如果滿足晉升到老年代閾值,則某些對象將被提升到老年代,並通過動態計算出下次GC時堆中年輕代的 Region個數,即年輕代內存大小來控制 Young GC的時間開銷。
Young GC的觸發時機
Young GC在Eden充滿時觸發,在回收之后所有之前屬於Eden的區塊全部變成空白即不屬於任何一個分區(Eden、 Survivor、Old)
1.3.2 Mixed GC
Mixed GC:將選擇所有年輕代里的 Region,外加根據 global concurrent marking統計得出收集收益高的若干老年代 Region進行GC(所以CSet里存的是所有年輕代里的 Region加上在全局並發標記階段標記出來的收益高的 Region)。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代 Region,以此來控制Mixed GC的時間開銷。
- 需要注意的是:Mixed GC不是 Full GC,它只能回收部分老年代的 Region而不是全部的Region,如果 Mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行 Mixed GC,就會退化然后使用 serial old GC( Full GC)來收集整個 GC heap。所以本質上,G1是不提供 Full GC的。所以總的來說,G1的運行過程是這樣的:會在 Young GC和Mixed GC之間不斷地切換運行,同時定期地做全局並發標記,在實在趕不上對象創建速度的情況下使用Full Gc( Serial GC)。
- 它的GC步驟分為兩步全局並發標記(global concurrent marking)和拷貝存活對象(evacuation)
Mixed gc 中的Global concurrent marking
Mixed gc 中的 global concurrent marking的執行過程類似於CMS,但是不同的是,在G1 GC中,它主要是為 Mixed GC提供標記服務的【老年代之所以知道為什么哪里的回收效益高就是根據此服務得出的結果確定的】,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分為四個步驟
- 初始標記( initial mark,STW):它標記了從GC Root開始直接可達的對象。第一階段 initial mark是共用了 Young GC的暫停,這是因為他們可以復用 root scan操作,所以可以說 global concurrent marking是伴隨 Young GC而發生的。
- 並發標記( Concurrent Marking):這個階段從GC Root開始對heap中的對象進行標記,標記線程與應用程序線程並發執行,並且收集各個Region的存活對象信息。
- 重新標記( Remark,STW) :標記那些在並發標記階段發生變化的對象,將被回收。
- 清理( Cleanup ):清除空 Region(沒有存活對象的),加入到 free list。第四階段 Cleanup只是回收了沒有存活對象的 Region,所以它並不需要STW
停頓預測模型
- G1收集器突出表現出來的一點是通過一個停頓預測模型根據用戶配置的停頓時間來選擇CSet的大小,從而達到用戶期待的應用程序暫停時間
- 通過-XX:MaxgcpauseMillis 參數來設置。這一點有點類似於 Parallel Scavenge收集器。關於停頓時間的設置並不是越短越好,設置的時間越短意味着每次收集的CSet越小,導致垃圾逐步積累變多,最終不得不退化成 Serial GC;停頓時間設置的過長那么會導致每次都會產生長時間的停頓影響了程序對外的響應時間
Mixed GC的觸發時機
觸發時機由一些參數控制,另外這些參數也控制着哪些老年代 Region會被選入CSet(收集集合)
- 參數:G1heapwastepercent: Global concurrent marking結束之后,我們可以知道 old gen regions中有多少空間要被回收在每次YGC之后和再次發生Mixed GC之前【YGC是在Eden空間滿了就進行GC】會檢査垃圾占比是否達到此參數,只有達到了,下次才會發生 Mixed GC
- 參數:G1MixedGCliveThresholdPercent: old generation region中的存活對象的占比,只有占比低於此參數,該old generation region才會被選入CSet
- 參數:G1MixedGCCountTarget:一次global concurrent marking之后,最多執行 MixedGC的次數
- 參數:G1OldCSetRegionThresholdPercent:一次Mxed GC中能被選入Cset的最多old generation region數量
1.3.3 G1的收集概覽
G1算法將堆划分為若干個區域( Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代和老年代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味着,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在
YGC收集時如何找到根對象呢?老年代所有的對象都是根對象嗎?那如果直接對整個老年代進行掃描的話會耗費大量的時間。於是G1引入了我們上面提到的RSet的概念,作用是跟蹤指向某個heap區內的對象引用。但是在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。YGC收集過程如下:,不懂的原因,對Rset 和point out point into 相關還是不理解
三色標記算法
這是Mixed gc 中的Global concurrent marking中的內容,提到並發標記,我們不得不了解並發標記的三色標記算法,mark的過程就是遍歷heap標記 live object的過程。它是描述追蹤式回收器的一種有效的方法,利用它可以推演回收器的正確性。我們將對象分成三種類型:
- 黑色:根對象或者該對象與它的子對象都被掃描過(即對象被標記了,且它的所有feld也被標記完了)
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象(即它的 field還沒有被標記或標記完)
- 白色:未被掃描對象,如果掃描完成所有對象之后最終為白色的為不可達對象,即垃圾對象(即對象沒有被標記到)
標記過程演示
可能出現的問題
-
-
這時候應用程序執行了以下操作A.c=C和B.c=null這樣,對象的狀態圖變成如下情形
-
-
這時候垃圾收集器再去標記掃描的時候就會變成下圖這樣,這樣就會造成漏標:
-
解決方法-SATB
SATB snapshot at the beginning
- 在開始的時候生成一個快照圖,標記存活的對象
- 在並發標記的時候所有被改變的對象入隊(在write barrier里把所有舊的引用所指向的對象都變成非白的,如上圖中,將B所指向的引用C變成非白的)
- 可能存在浮動垃圾,將在下次垃圾收集時被收集
SATB詳解
-
SATB詳解SATB是維持並發GC的一種手段。G1並發的基礎就是SATB。SATB可以理解成在GC開始之前對堆內存里的對象做一次快照,此時活的對象就認為是活的,從而形成個對象圖。在GC收集的時候,新生代的對象也認為是活的對象,除此之外其他不可達的對象都認為是垃圾對象
-
如何找到在GC過程中分配的對象呢? 這是收集過程中可能遇到的第二個問題,每個Region記錄着兩個top-at-mark- start(TAMS)指針,分別為 prevtamst和 next AMS。在TAMS以上的對象就是新分配的,因而被視為隱式 marked。通過這種方式我們就找到了在GC過程中新分配的對象,並把這些對象認為是活的對象
-
解決了對象在GC過程中分配的問題,那么在GC過程中引用發生變化的問題怎么解決呢?Write Barrier就是對引用字段進行賦值做額外處理。通過 Write Barrier就可以了解到哪些引用對象發生了什么樣的變化。
-
SATB僅僅對於在 marking開始階段進行snapshot"(marked all reachable at markstart)),但是 concurrent的時候並發修改可能造成對象漏標記
- 對 black新引用了一個 white對象,然后又從gray對象中刪除了對該 white對象的引用這樣會造成了該whie對象漏標記
- 對 black新引用了一個 white對象,然后從gray對象刪了一個引用該 white對象的 white對象,這樣也會造成了該 white對象漏標記
- 對 black新引用了一個剛new出來的 white對象,沒有其他gray對象引用該 White對象這樣也會造成了該 white對象漏標記
- 對於三色算法在 concurrent的時候可能產生的漏標記問題,SATB在 marking階段中對於從gray對象移除的目標引用對象標記為gray,對於 black引用的新產生的對象標記為 black;由於是在開始的時候進行snapshot,因而可能存在 Floating Garbage
-
漏標與誤標
- 誤標沒什么關系,頂多造成浮動垃圾,在下次gc還是可以回收的,但是漏標的后果是致命的把本應該存活的對象給回收了,從而影響的程序的正確性
- 漏標的情況只會發生在白色對象中,且滿足以下任意一個條件
- 並發標記時,應用線程給一個黑色對象的引用類型字段賦值了該白色對象
- 並發標記時,應用線程刪除所有灰色對象到該白色對象的引用(但是此時或許有黑色對象引用該白色對象)
- 如何解決以上問題?
- 對於第一種情況,利用 post-write barrier,記錄所有新增的引用關系,然后根據這些引用關系為根重新掃描一遍,Write Barrier就是對引用字段進行賦值做額外處理。通過 Write Barrier就可以了解到哪些引用對象發生了什么樣的變化
- 對於第二種情況,利用pre-write barrier,將所有即將被刪除的引用關系的舊引用記錄下來,最后以這些舊引用為根重新掃描遍,這樣就能掃描標記到那個“白色對象”
1.4 G1最佳實踐
1.4.1 不斷調優暫停時間指標
通過 XX. MaxGcPauseMillis=x 可以設置啟動應用程序暫停的時間,G1在運行的時候會根據這個參數選擇Cset來滿足響應時間的設置。一般情況下這個值設置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設置成50ms就不太合理。暫停時間設置的太短,就會導致出現G1跟不上垃圾產生的速度。最終退化成 Full gc。所以對這個參數的調優是一個持續的過程,逐步調整到最佳狀態。
1.4.2 不要設置新生代和老年代的大小
G1收集器在運行的時候會調整新生代和老年代的大小。通過改變代的大小來調整對象晉升的速度以及晉升年齡,從而達到我們為收集器設置的暫停時間目標。設置了新生代大小相當於放棄了G1為我們做的自動調優。我們需要做的只是設置整個堆內存的大小,剩下的交給G1自己去分配各個代的大小即可
1.4.3 關注 Evacuation Failure
Evacuation Failure【Evacuation是拷貝存活對象的意思,是GC步驟之一,GC步驟分為兩步全局並發標記(global concurrent marking)和拷貝存活對象(evacuation)】 類似於CMS里面的晉升失敗堆空間的垃圾太多導致無法完成 Region之間的拷貝,於是不得不退化成 Full GC來做一次全局范圍內的垃圾收集
對比其它收集器
- G1 VS CMS對比使用mark- sweep的CMS,G1使用的copying算法不會造成內存碎片
- 對比 Parallel Scavenge(基於 copying)Parallel Old收集器(基於mark- compact-sweep), Parallel會對整個區域做整理導致gc停頓會比較長,而G1只是特定地整理壓縮某些Region
- G1並非一個實時的收集器,與 parallel Scavenge一樣,對gc停頓時間的設置並不絕對生效,只是G1有較高的幾率保證不超過設定的gc停頓時間。與之前的gc收集器對比,G1會根據用戶設定的gc停頓時間智能評估哪幾個 region需要被回收可以滿足用戶的設定
1.5 實驗
實驗代碼
實驗參數
實驗結果
2020-03-03T11:06:20.055+0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0024128 secs]
[Parallel Time: 1.3 ms, GC Workers: 8]
# GC Workers: 8 ,當前的GC線程數為8
[GC Worker Start (ms): Min: 178.9, Avg: 178.9, Max: 178.9, Diff: 0.1]
# GC線程開始
[Ext Root Scanning (ms): Min: 0.3, Avg: 0.4, Max: 0.5, Diff: 0.3, Sum: 3.2]
# 根節點掃描
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
# 更新RSet
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.6, Avg: 0.7, Max: 0.8, Diff: 0.2, Sum: 5.8]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
# 處理引用隊列
[Termination Attempts: Min: 1, Avg: 6.0, Max: 9, Diff: 8, Sum: 48]
# 此以上的步驟都是和之前說到的YGC的處理流程是一一對應的
[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.9]
[GC Worker Total (ms): Min: 1.2, Avg: 1.2, Max: 1.3, Diff: 0.1, Sum: 10.0]
[GC Worker End (ms): Min: 180.2, Avg: 180.2, Max: 180.2, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
# 即clear CartTable
[Other: 1.0 ms]
[Choose CSet: 0.0 ms]
# 選擇要回收的Region
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
# 引用的處理
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 2048.0K(6144.0K)->0.0B(2048.0K) Survivors: 0.0B->1024.0K Heap: 3990.1K(10.0M)->2847.6K(10.0M)]
# 表示執行完YGC之后,整個eden空間的結果,可以看到執行完之后eden空間變成了0;Survivor空間變成1m,這是從新生代來的
[Times: user=0.00 sys=0.00, real=0.00 secs]
2020-03-03T11:06:20.058+0800: [GC concurrent-root-region-scan-start]
2020-03-03T11:06:20.059+0800: [GC pause (G1 Humongous Allocation) (young)2020-03-03T11:06:20.059+0800: [GC concurrent-root-region-scan-end, 0.0011467 secs]
2020-03-03T11:06:20.059+0800: [GC concurrent-mark-start]
, 0.0019177 secs]
# 以上是並發的一些處理
[Root Region Scan Waiting: 0.5 ms]
[Parallel Time: 0.9 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 182.5, Avg: 182.6, Max: 182.8, Diff: 0.3]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.2]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.4, Avg: 0.4, Max: 0.5, Diff: 0.1, Sum: 3.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Termination Attempts: Min: 1, Avg: 4.8, Max: 8, Diff: 7, Sum: 38]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.4, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.2]
[GC Worker End (ms): Min: 183.3, Avg: 183.3, Max: 183.4, Diff: 0.1]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(2048.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 3912.6K(10.0M)->3895.4K(10.0M)]
# 第二次YGC的結果
[Times: user=0.00 sys=0.00, real=0.00 secs]
2020-03-03T11:06:20.061+0800: [GC concurrent-mark-end, 0.0015466 secs]
2020-03-03T11:06:20.061+0800: [Full GC (Allocation Failure) 3895K->3731K(10M), 0.0042062 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 3895.4K(10.0M)->3731.8K(10.0M)], [Metaspace: 3113K->3113K(1056768K)]
[Times: user=0.03 sys=0.00, real=0.00 secs]
2020-03-03T11:06:20.066+0800: [GC remark, 0.0000313 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2020-03-03T11:06:20.066+0800: [GC concurrent-mark-abort]
完成了
Heap
garbage-first heap total 10240K, used 4755K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
region size 1024K, 1 young (1024K), 0 survivors (0K)
# 堆的空間分配情況
Metaspace used 3238K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0