自動(dòng)的內(nèi)存管理系統(tǒng)實(shí)操手冊(cè)——Java和Golang對(duì)比篇

一、 垃圾回收區(qū)域

Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域隨著線程而生,隨著線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧的操作,每個(gè)棧幀中分配多少內(nèi)存基本是在類(lèi)結(jié)構(gòu)確定下來(lái)時(shí)就已知的。而Java堆和方法區(qū)則不同,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能不同,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間時(shí)才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,因此,Java堆和方法區(qū)是Java垃圾收集器管理的主要區(qū)域。

Go內(nèi)存會(huì)分成堆區(qū)(Heap)和棧區(qū)(Stack)兩個(gè)部分,程序在運(yùn)行期間可以主動(dòng)從堆區(qū)申請(qǐng)內(nèi)存空間,這些內(nèi)存由內(nèi)存分配器分配并由垃圾收集器負(fù)責(zé)回收。棧區(qū)的內(nèi)存由編譯器自動(dòng)進(jìn)行分配和釋放,棧區(qū)中存儲(chǔ)著函數(shù)的參數(shù)以及局部變量,它們會(huì)隨著函數(shù)的創(chuàng)建而創(chuàng)建,函數(shù)的返回而銷(xiāo)毀。如果只申請(qǐng)和分配內(nèi)存,內(nèi)存終將枯竭。Go使用垃圾回收收集不再使用的span,把span釋放交給mheap,mheap對(duì)span進(jìn)行span的合并,把合并后的span加入scav樹(shù)中,等待再分配內(nèi)存時(shí),由mheap進(jìn)行內(nèi)存再分配。因此,Go堆是Go垃圾收集器管理的主要區(qū)域。
二、 觸發(fā)垃圾回收的時(shí)機(jī)
Java當(dāng)應(yīng)用程序空閑時(shí),即沒(méi)有應(yīng)用線程在運(yùn)行時(shí),GC會(huì)被調(diào)用。因?yàn)镚C在優(yōu)先級(jí)最低的線程中進(jìn)行,所以當(dāng)應(yīng)用忙時(shí),GC線程就不會(huì)被調(diào)用,但以下條件除外。
Java堆內(nèi)存不足時(shí),GC會(huì)被調(diào)用。但是這種情況由于java是分代收集算法且垃圾收集器種類(lèi)十分多,因此其觸發(fā)各種垃圾收集器的GC時(shí)機(jī)可能不完全一致,這里我們說(shuō)的為一般情況。
1. 當(dāng)Eden區(qū)空間不足時(shí)Minor GC;
2. 對(duì)象年齡增加到一定程度時(shí)Young GC;
3. 新生代對(duì)象轉(zhuǎn)入老年代及創(chuàng)建為大對(duì)象、大數(shù)組時(shí)會(huì)導(dǎo)致老年代空間不足,觸發(fā)Old GC;
4. System.gc()調(diào)用觸發(fā)Full GC;
5. 各種區(qū)塊占用超過(guò)閾值的情況。
Go則會(huì)根據(jù)以下條件進(jìn)行觸發(fā):
runtime.mallocgc申請(qǐng)內(nèi)存時(shí)根據(jù)堆大小觸發(fā)GC;
runtime.GC用戶程序手動(dòng)觸發(fā)GC;
runtime.forcegchelper后臺(tái)運(yùn)行定時(shí)檢查觸發(fā)GC。
三、收集算法
當(dāng)前Java虛擬機(jī)的垃圾收集采用分代收集算法,根據(jù)對(duì)象存活周期的不同將內(nèi)存分為幾塊。比如在新生代中,每次收集都會(huì)有大量對(duì)象死去,所以可以選擇“標(biāo)記-復(fù)制”算法,只需要付出少量對(duì)象的復(fù)制成本就可以完成每次垃圾收集。而老年代的對(duì)象存活幾率是比較高的,而且沒(méi)有額外的空間對(duì)它進(jìn)行分配擔(dān)保,所以我們必須選擇“標(biāo)記-清除”或“標(biāo)記-整理”算法進(jìn)行垃圾收集。
當(dāng)前Go的都是基于標(biāo)記清除算法進(jìn)行垃圾回收。
四、垃圾碎片處理
由于Java的內(nèi)存管理劃分,因此容易產(chǎn)生垃圾對(duì)象,JVM這些年不斷的改進(jìn)和更新GC算法,JVM在處理內(nèi)存碎片問(wèn)題上更多采用空間壓縮和分代收集的思想,例如在新生代使用“標(biāo)記-復(fù)制”算法,G1收集器支持了對(duì)象移動(dòng)以消減長(zhǎng)時(shí)間運(yùn)行的內(nèi)存碎片問(wèn)題,劃分region的設(shè)計(jì)更容易把空閑內(nèi)存歸還給OS等設(shè)計(jì)。
由于Go的內(nèi)存管理的實(shí)現(xiàn),很難實(shí)現(xiàn)分代,而移動(dòng)對(duì)象也可能會(huì)導(dǎo)致runtime更龐大復(fù)雜,因此Go在關(guān)于內(nèi)存碎片的處理方案和Java并不太一樣。
1.Go語(yǔ)言span內(nèi)存池的設(shè)計(jì),減輕了很多內(nèi)存碎片的問(wèn)題。
Go內(nèi)存釋放的過(guò)程如下:當(dāng)mcache中存在較多空閑span時(shí),會(huì)歸還給 mcentral;而mcentral中存在較多空閑span時(shí),會(huì)歸還給mheap;mheap再歸還給操作系統(tǒng)。這種設(shè)計(jì)主要有以下幾個(gè)優(yōu)勢(shì):
內(nèi)存分配大多時(shí)候都是在用戶態(tài)完成的,不需要頻繁進(jìn)入內(nèi)核態(tài)。
每個(gè) P 都有獨(dú)立的 span cache,多個(gè) CPU 不會(huì)并發(fā)讀寫(xiě)同一塊內(nèi)存,進(jìn)而減少 CPU L1 cache 的 cacheline 出現(xiàn) dirty 情況,增大 cpu cache 命中率。
內(nèi)存碎片的問(wèn)題,Go是自己在用戶態(tài)管理的,在 OS 層面看是沒(méi)有碎片的,使得操作系統(tǒng)層面對(duì)碎片的管理壓力也會(huì)降低。
mcache 的存在使得內(nèi)存分配不需要加鎖。
2.tcmalloc分配機(jī)制,Tiny對(duì)象和大對(duì)象分配優(yōu)化,在某種程度上也導(dǎo)致基本沒(méi)有內(nèi)存碎片會(huì)出現(xiàn)。
比如常規(guī)上sizeclass=1的span,用來(lái)給<=8B 的對(duì)象使用,所以像 int32, byte, bool以及小字符串等常用的微小對(duì)象,都會(huì)使用sizeclass=1的span,但分配給他們8B的空間,大部分是用不上的。并且這些類(lèi)型使用頻率非常高,就會(huì)導(dǎo)致出現(xiàn)大量的內(nèi)部碎片。
因此Go盡量不使用sizeclass=1的span,而是將<16B的對(duì)象為統(tǒng)一視為tiny對(duì)象。分配時(shí),從sizeclass=2的span中獲取一個(gè)16B的object用以分配。如果存儲(chǔ)的對(duì)象小于16B,這個(gè)空間會(huì)被暫時(shí)保存起來(lái) (mcache.tiny字段),下次分配時(shí)會(huì)復(fù)用這個(gè)空間,直到這個(gè)object用完為止。

以上圖為例,這樣的方式空間利用率是(1+2+8)/16*100%= 68.75%,而如果按照原始的管理方式,利用率是(1+2+8)/(8*3)=45.83%。源碼中注釋描述,說(shuō)是對(duì)tiny對(duì)象的特殊處理,平均會(huì)節(jié)省20%左右的內(nèi)存。如果要存儲(chǔ)的數(shù)據(jù)里有指針,即使<= 8B也不會(huì)作為tiny對(duì)象對(duì)待,而是正常使用sizeclass=1的span。
Go中,最大的sizeclass最大只能存放32K的對(duì)象。如果一次性申請(qǐng)超過(guò)32K的內(nèi)存,系統(tǒng)會(huì)直接繞過(guò)mcache和mcentral,直接從mheap上獲取,mheap中有一個(gè)freelarge字段管理著超大span。
3.Go的對(duì)象(即struct類(lèi)型)是可以分配在棧上的。
Go會(huì)在編譯時(shí)做靜態(tài)逃逸分析(Escape Analysis), 如果發(fā)現(xiàn)某個(gè)對(duì)象并沒(méi)有逃出當(dāng)前作用域,則會(huì)將對(duì)象分配在棧上而不是堆上,從而減輕了GC內(nèi)存碎片回收壓力。
比如如下代碼:
func F() {temp := make([]int, 0, 20) //只是內(nèi)函數(shù)內(nèi)部申請(qǐng)的臨時(shí)變量,并不會(huì)作為返回值返回,它就是被編譯器申請(qǐng)到棧里面。temp = append(temp, 1)}func main() {F()}
hewittwang@HEWITTWANG-MB0 rtx % go build -gcflags=-m# hello./new1.go:4:6: can inline F./new1.go:9:6: can inline main./new1.go:10:3: inlining call to F./new1.go:5:14: make([]int, 0, 20) does not escape./new1.go:10:3: make([]int, 0, 20) does not escapeh
package mainimport "fmt"func F() {temp := make([]int, 0, 20)fmt.Print(temp)}func main() {F()}
運(yùn)行代碼如下,結(jié)果顯示temp變量被分配在堆上,這是由于temp傳入了print函數(shù)里,編譯器會(huì)認(rèn)為變量之后還會(huì)被使用。因此就申請(qǐng)到堆上,申請(qǐng)到堆上面的內(nèi)存才會(huì)引起垃圾回收,如果這個(gè)過(guò)程(特指垃圾回收不斷被觸發(fā))過(guò)于高頻就會(huì)導(dǎo)致GC壓力過(guò)大,程序性能出問(wèn)題。
hewittwang@HEWITTWANG-MB0 rtx % go build -gcflags=-m# hello./new1.go:9:11: inlining call to fmt.Print./new1.go:12:6: can inline main./new1.go:8:14: make([]int, 0, 20) escapes to heap./new1.go:9:11: temp escapes to heap./new1.go:9:11: []interface {}{...} does not escape<autogenerated>:1: .this does not escape
五、“GC Roots” 的對(duì)象選擇
在Java中由于內(nèi)存運(yùn)行時(shí)區(qū)域的劃分,通常會(huì)選擇以下幾種作為“GC Roots” 的對(duì)象:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;
本地方法棧(Native 方法)中引用的對(duì)象;
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象;
方法區(qū)中常量引用的對(duì)象;
Java虛擬機(jī)內(nèi)部引用;
所有被同步鎖持有的對(duì)象。
而在Java中的不可達(dá)對(duì)象有可能會(huì)逃脫。即使在可達(dá)性分析法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑階段”,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程;此外Java中由于存在運(yùn)行時(shí)常量池和類(lèi),因此也需要對(duì)運(yùn)行時(shí)常量池和方法區(qū)的類(lèi)進(jìn)行清理。
而Go的選擇就相對(duì)簡(jiǎn)單一點(diǎn),即全局變量和G Stack中的引用指針,簡(jiǎn)單來(lái)說(shuō)就是全局量和go程中的引用指針。因?yàn)镚o中沒(méi)有類(lèi)的封裝概念,因而GC Root選擇也相對(duì)簡(jiǎn)單一些。
六、寫(xiě)屏障
為了解決并發(fā)三色可達(dá)性分析中的懸掛指針問(wèn)題,出現(xiàn)了2種解決方案,分別是分別是“Dijkstra插入寫(xiě)屏障”和“Yuasa刪除寫(xiě)屏障”。
在java中,對(duì)上述2種方法都有應(yīng)用,比如CMS是基于“Dijkstra插入寫(xiě)屏障”做并發(fā)標(biāo)記的,G1、Shenandoah則是使用“Yuasa刪除寫(xiě)屏障”來(lái)實(shí)現(xiàn)的。
在Go語(yǔ)言v1.7版本之前,運(yùn)行時(shí)會(huì)使用Dijkstra插入寫(xiě)屏障保證強(qiáng)三色不變性,Go語(yǔ)言在v1.8組合Dijkstra插入寫(xiě)屏障和Yuasa刪除寫(xiě)屏障構(gòu)成了混合寫(xiě)屏障,混合寫(xiě)屏障結(jié)合兩者特點(diǎn),通過(guò)以下方式實(shí)現(xiàn)并發(fā)穩(wěn)定的GC:
1.將棧上的對(duì)象全部掃描并標(biāo)記為黑色。
2.GC期間,任何在棧上創(chuàng)建的新對(duì)象,均為黑色。
3.被刪除的對(duì)象標(biāo)記為灰色。
4.被添加的對(duì)象標(biāo)記為灰色。
由于要保證棧的運(yùn)行效率,混合寫(xiě)屏障是針對(duì)于堆區(qū)使用的。即棧區(qū)不會(huì)觸發(fā)寫(xiě)屏障,只有堆區(qū)觸發(fā),由于棧區(qū)初始標(biāo)記的可達(dá)節(jié)點(diǎn)均為黑色節(jié)點(diǎn),因而也不需要第二次STW下的掃描。本質(zhì)上是融合了插入屏障和刪除屏障的特點(diǎn),解決了插入屏障需要二次掃描的問(wèn)題。同時(shí)針對(duì)于堆區(qū)和棧區(qū)采用不同的策略,保證棧的運(yùn)行效率不受損。
七、總結(jié)
| Java | Go | |
| GC區(qū)域 | Java堆和方法區(qū) | Go堆 |
| 出發(fā)GC時(shí)機(jī) | 分代收集導(dǎo)致觸發(fā)時(shí)機(jī)很多 | 申請(qǐng)內(nèi)存、手動(dòng)觸發(fā)、定時(shí)觸發(fā) |
| 垃圾收集算法 | 分代收集。在新生代(“標(biāo)記-復(fù)制”); 老年代(“標(biāo)記-清除”或“標(biāo)記-整理”) | 標(biāo)記清除算法 |
| 垃圾種類(lèi) | 死亡對(duì)象(可能會(huì)逃脫)、廢棄常量和無(wú)用的類(lèi) | 全局變量和G Stack中的引用指針 |
| 標(biāo)記階段 | 三色可達(dá)性分析算法(插入寫(xiě)屏障,刪除寫(xiě)屏障) | 三色可達(dá)性分析算法(混合寫(xiě)屏障) |
| 空間壓縮整理 | 是 | 否 |
| 內(nèi)存分配 | 指針碰撞/空閑列表 | span內(nèi)存池 |
| 垃圾碎片解決方案 | 分代GC、對(duì)象移動(dòng)、劃分region等設(shè)計(jì) | Go語(yǔ)言span內(nèi)存池、tcmalloc分配機(jī)制、對(duì)象可以分配在棧上、對(duì)象池 |
從垃圾回收的角度來(lái)說(shuō),經(jīng)過(guò)多代發(fā)展,Java的垃圾回收機(jī)制較為完善,Java劃分新生代、老年代來(lái)存儲(chǔ)對(duì)象。對(duì)象通常會(huì)在新生代分配內(nèi)存,多次存活的對(duì)象會(huì)被移到老年代,由于新生代存活率低,產(chǎn)生空間碎片的可能性高,通常選用“標(biāo)記-復(fù)制”作為回收算法,而老年代存活率高,通常選用“標(biāo)記-清除”或“標(biāo)記-整理”作為回收算法,壓縮整理空間。
Go是非分代的、并發(fā)的、基于三色標(biāo)記和清除的垃圾回收器,它的優(yōu)勢(shì)要結(jié)合它tcmalloc內(nèi)存分配策略才能體現(xiàn)出來(lái),因?yàn)樾∥?duì)象的分配均有自己的內(nèi)存池,所有的碎片都能被完美復(fù)用,所以GC不用考慮空間碎片的問(wèn)題。
參考文獻(xiàn)
1.《Go語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)》
(https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/)
2.《一個(gè)專(zhuān)家眼中的Go與Java垃圾回收算法大對(duì)比》
(https://blog.csdn.net/u011277123/article/details/53991572)
3.《Go語(yǔ)言問(wèn)題集》
(https://www.bookstack.cn/read/qcrao-Go-Questions/spilt.19.GC-GC.md)
4.《CMS垃圾收集器》
(https://juejin.cn/post/6844903782107578382)
5.《Golang v 1.16版本源碼》
(https://github.com/golang/go)
6.《Golang---內(nèi)存管理(內(nèi)存分配)》
(http://t.zoukankan.com/zpcoding-p-13259943.html)
7.《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第3版)》—機(jī)械工業(yè)出版社
作者簡(jiǎn)介
汪匯
騰訊后臺(tái)開(kāi)發(fā)工程師
騰訊后臺(tái)開(kāi)發(fā)工程師,負(fù)責(zé)騰訊看點(diǎn)相關(guān)后端業(yè)務(wù),畢業(yè)于南京大學(xué)軟件學(xué)院。
推薦閱讀
自動(dòng)的內(nèi)存管理系統(tǒng)實(shí)操手冊(cè)——Golang垃圾回收篇
自動(dòng)的內(nèi)存管理系統(tǒng)實(shí)操手冊(cè)——Java垃圾回收篇
百萬(wàn)級(jí)庫(kù)表能力!這個(gè)MongoDB為什么可以這么牛?


