目錄
- 一、概述
- 二、運行時數據區
- 方法區
- 運行時常量池
- 堆
- 棧
- 本地方法棧
- 程序計數器
- 三、對象訪問
- 四、垃圾回收
- 如何定義垃圾
- 1、引用計數法
- 2、可達性分析
- 垃圾回收方法
- 1、Mark-Sweep標記-清除算法
- 2、Copying復制算法
- 3、Mark-Compact標記-整理算法
- 4、Generational Collection 分代收集
- 垃圾收集器
- 1、Serial收集器
- 2、ParNew收集器
- 3、Parallel Scavenge收集器
- 4、并發標記清除(Concurrent Mark Sweep Collector, CMS)收集器
一、概述
對于從事C和C++程序開發的開發人員來說,在內存管理領域,他們既是擁有最高權力的皇帝,又是從事最基礎工作的勞動人民—既擁有每一個對象的“所有權”,又擔負著每一個對象生命開始到終結的維護責任。
對于Java程序員來說,在虛擬機的自動內存管理機制的幫助下,不再需要為每一個new操作去寫配對的delete/free代碼,而且不容易出現內存泄漏和內存溢出問題,看起來由虛擬機管理內存一切都很美好。不過,也正是因為Java程序員把內存控制的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不了解虛擬機是怎樣使用內存的,那排查錯誤將會成為一項異常艱難的工作。
二、運行時數據區
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機進程的啟動而存在,有些區域則是依賴用戶線程的啟動和結束而建立和銷毀。根據《Java虛擬機規范(第2版)》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時數據區域,如下圖所示:
方法區
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
對于習慣在HotSpot虛擬機上開發和部署程序的開發者來說,很多人愿意把方法區稱為“永久代”(Permanent Generation),本質上兩者并不等價,僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對于其他虛擬機(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。即使是HotSpot虛擬機本身,根據官方發布的路線圖信息,現在也有放棄永久代并“搬家”至Native Memory來實現方法區的規劃了。
Java虛擬機規范對這個區域的限制非常寬松,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。在Sun公司的BUG列表中,
曾出現過的若干個嚴重的BUG就是由于低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏。 根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中
。 Java虛擬機對Class文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用于存儲哪種數據都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行。但對于運行時常量池,Java虛擬機規范沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。不過,一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。 運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只能在編譯期產生,也就是并非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。 既然運行時常量池是方法區的一部分,自然會受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。
堆
對于大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存
。這一點在Java虛擬機規范中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”(Garbage Collected Heap,幸好國內沒翻譯成“垃圾堆”)。如果從內存回收的角度看,由于現在收集器基本都是采用的 分代收集算法
,所以Java堆中還可以細分為: 新生代和老年代
;再細致一點的有 Eden空間、From Survivor空間、To Survivor空間
等。如果從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。不過,無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步劃分的目的是為了更好地回收內存,或者更快地分配內存。在本章中,我們僅僅針對內存區域的作用進行討論,Java堆中的上述各個區域的分配和回收等細節將會是下一章的主題。
根據Java虛擬機規范的規定,Java堆可以處于物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
棧
Java虛擬機棧(Java Virtual Machine Stacks)是線程私有的,它的生命周期與線程相同
。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息
。每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
經常有人把Java內存區分為堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這復雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關系最密切的內存區域是這兩塊。其中所指的“堆”就是Java堆,而所指的“棧”就是現在講的虛擬機棧,或者說是虛擬機棧中的局部變量表部分。
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型),它不等同于對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。 在Java虛擬機規范中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError異常。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務
。虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器
。在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。 由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。 如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie方法,這個計數器值則為空(Undefined)。此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
堆和棧分開設計是為什么呢?
-
棧存儲了處理邏輯,堆存儲了具體的數據,這樣隔離設計更為清晰;
-
堆與棧分離,使得堆可以被多個棧共享;
-
棧保存了上下文信息,因此只能向上增長,而堆是動態分配的。
總結:
名稱 | 特征 | 作用 | 配置 | 異常 |
---|---|---|---|---|
棧區 | 線程私有,使用一段連續的內存空間 | 存放局部變量表、操作棧、動態鏈接、方法出口 | -XSs | StackOverflowError、OutOfMemoryError |
堆 | 線程共享,生命周期與虛擬機相同 | 保存對象實例 | -Xms -Xmx -Xmn | OutOfMemoryError |
程序計數器 | 線程私有,占用內存小 | 字節碼行號 | 無 | 無 |
方法區 | 線程共享 | 存儲類加載信息、常量、靜態變量等 | -XX:PermSize -XX:MaxPermSize | OutOfMemoryError |
三、對象訪問
介紹完Java虛擬機的運行時數據區之后,我們就可以來探討一個問題:在Java語言中,對象訪問是如何進行的?對象訪問在Java語言中無處不在,是最普通的程序行為,但即使是最簡單的訪問,也會卻涉及Java棧、Java堆、方法區這三個最重要內存區域之間的關聯關系,如下面的這句代碼:
Object obj = new Object();
假設這句代碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java棧的本地變量表中,作為一個reference類型數據出現。而“new Object()”這部分的語義將會反映到Java堆中,形成一塊存儲了Object類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存布局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在Java堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。
由于reference類型在Java虛擬機規范里面只規定了一個指向對象的引用,并沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針
。 如果使用句柄訪問方式,Java堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息,如下圖所示:
如果使用的是直接指針訪問方式,Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象地址,如下圖所示:
這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項非常可觀的執行成本。就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的范圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。
四、垃圾回收
如何定義垃圾
有兩種方式:引用計數(但是無法解決循環引用的問題)、可達性分析
。
判斷對象可以回收的情況:
-
顯示的把某個引用置位NULL或者指向別的對象
-
局部引用指向的對象
-
弱引用關聯的對象
1、引用計數法
這個算法的實現是,給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值為0的對象就是不可能再被使用的。這種算法使用場景很多,但是,Java中卻沒有使用這種算法,因為 這種算法很難解決對象之間相互引用的情況
。
2、可達性分析
這個算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的
。
那么問題又來了,如何選取GCRoots對象呢?在Java語言中,可以作為GCRoots的對象包括下面幾種:
-
虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。
-
方法區中的類靜態屬性引用的對象。
-
方法區中常量引用的對象。
-
本地方法棧中JNI(Native方法)引用的對象。
下面給出一個GCRoots的例子,如下圖,為GCRoots的引用鏈。
由圖可知,obj8、obj9、obj10都沒有到GCRoots對象的引用鏈,即便obj9和obj10之間有引用鏈,他們還是會被當成垃圾處理,可以進行回收。
垃圾回收方法
1、Mark-Sweep標記-清除算法
這是最基礎的算法,標記-清除算法就如同它的名字樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,標記完成后統一回收所有被標記的對象
。這種算法的不足主要體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除后會產生大量不連續的內存碎片, 內存碎片太多可能會導致以后程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作。標記-清除算法執行過程如圖:
這種方法優點:減少停頓時間
,但是缺點:會造成內存碎片
。
2、Copying復制算法
復制算法是為了解決效率問題而出現的,它將可用的內存分為兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已經使用過的內存空間一次性清理掉
。這樣每次只需要對整個半區進行內存回收,內存分配時也不需要考慮內存碎片等復雜情況,只需要移動指針,按照順序分配即可。復制算法的執行過程如圖:
不過這種算法有個缺點,內存縮小為了原來的一半,這樣代價太高了
。現在的商用虛擬機都采用這種算法來 回收新生代
,不過研究表明1:1的比例非常不科學,因此 新生代的內存被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor
。每次回收時,將Eden和Survivor中還存活著的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例為8:1,意思是每次新生代中可用內存空間為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)。
3、Mark-Compact標記-整理算法
復制算法在對象存活率較高的場景下要進行大量的復制操作,效率很低。萬一對象100%存活,那么需要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,因此一般不能直接選用復制算法。根據老年代的特點,有人提出了另外一種標記-整理算法,過程與標記-清除算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內存
。標記-整理算法的工作過程如圖:
這種方法可以解決內存碎片問題,但是會增加停頓時間。
4、Generational Collection 分代收集
現代商用虛擬機基本都采用 分代收集算法來進行垃圾回收
。這種算法沒什么特別的,無非是上面內容的結合罷了,根據對象的生命周期的不同將內存劃分為幾塊,然后根據各塊的特點采用最適當的收集算法。大批對象死去、少量對象存活的(新生代),使用復制算法,復制成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),采用標記-清理算法或者標記-整理算法
。
上面可以看到堆分成兩個個區域:
-
新生代(Young Generation)
:用于存放新創建的對象,采用復制回收方法,如果在s0和s1之間復制一定次數后,轉移到年老代中。這里的垃圾回收叫做minor GC; -
年老代(Old Generation)
:這些對象垃圾回收的頻率較低,采用的標記整理方法,這里的垃圾回收叫做 major GC。
這里可以詳細的說一下新生代復制回收的算法流程:
在新生代中,分為三個區:Eden,from survivor,to survior。
-
當觸發minor GC時,會先把Eden中存活的對象復制到to Survivor中;
-
然后再看from survivor,如果次數達到年老代的標準,就復制到年老代中;如果沒有達到則復制到to survivor中,如果to survivor滿了,則復制到年老代中。
-
然后調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待對象復制到那里的。
垃圾收集器
垃圾收集器就是上面講的理論知識的具體實現了。不同虛擬機所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機所包含的所有收集器如圖:
上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機所處的區域說明它是屬于新生代收集器還是老年代收集器。多說一句,我們必須明確一個觀點:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot為什么要實現這么多收集器的原因。OK,下面一個一個看一下收集器。
1、Serial收集器
最基本、發展歷史最久的收集器,這個收集器是一個 采用復制算法的單線程的收集器,單線程一方面意味著它只會使用一個CPU或一條線程去完成垃圾收集工作,另一方面也意味著它進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束為止
。后者意味著,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對很多應用是難以接受的。不過實際上到目前為止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器
,因為它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。Serial收集器運行過程如下圖所示:
說明:1. 需要STW(Stop The World),停頓時間長。2. 簡單高效,對于單個CPU環境而言,Serial收集器由于沒有線程交互開銷,可以獲取最高的單線程收集效率。
2、ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其余行為和Serial收集器完全一樣,包括使用的也是復制算法
。ParNew收集器除了多線程以外和Serial收集器并沒有太多創新的地方,但是它卻是Server模式下的虛擬機首選的新生代收集器
,其中有一個很重要的和性能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認為有劃時代意義的垃圾收集器,因為它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由于線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨著可用CPU數量的增加,它對于GC時系統資源的有效利用還是很有好處的。它默認開啟的收集線程數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。ParNew收集器運行過程如下圖所示:
3、Parallel Scavenge收集器
Parallel Scavenge收集器也是一個新生代收集器,也是用復制算法的收集器,也是并行的多線程收集器
,但是它的特點是它的關注點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量
。所謂吞吐量的意思就是CPU用于運行用戶代碼時間與CPU總消耗時間的比值,即 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
,虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器
。
停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,盡快完成運算任務,主要適合在后臺運算而不需要太多交互的任務。
虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以為前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由于與吞吐量關系密切,Parallel Scavenge收集器也被稱為“吞吐量優先收集器”。Parallel Scavenge收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開之后,就不需要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行情況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。如果對于垃圾收集器運作原理不太了解,以至于在優化比較困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇
。
4、并發標記清除(Concurrent Mark Sweep Collector, CMS)收集器
CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間為目標的收集器。使用標記 - 清除算法,收集過程分為如下四步:
(1)初始標記,標記GCRoots能直接關聯到的對象,時間很短。
(2)并發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。
(3)重新標記,修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,時間較長。
(4)并發清除,回收內存空間,時間很長。
其中,并發標記與并發清除兩個階段耗時最長,但是可以與用戶線程并發執行。運行過程如下圖所示: