<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 5980字,需瀏覽 12分鐘

           ·

          2021-08-09 04:01


          導(dǎo)語(yǔ) | 現(xiàn)代高級(jí)編程語(yǔ)言管理內(nèi)存的方式分自動(dòng)和手動(dòng)兩種。手動(dòng)管理內(nèi)存的典型代表是C和C++,編寫(xiě)代碼過(guò)程中需要主動(dòng)申請(qǐng)或者釋放內(nèi)存;而PHP、Java 和Go等語(yǔ)言使用自動(dòng)的內(nèi)存管理系統(tǒng),由內(nèi)存分配器和垃圾收集器來(lái)代為分配和回收內(nèi)存,其中垃圾收集器就是我們常說(shuō)的GC。在《自動(dòng)的內(nèi)存管理系統(tǒng)實(shí)操手冊(cè)——Java垃圾回收篇》和《自動(dòng)的內(nèi)存管理系統(tǒng)實(shí)操手冊(cè)——Golang垃圾回收篇》向大家分享了Java 和 Golang 垃圾回收算法之后,今天騰訊后臺(tái)開(kāi)發(fā)工程師汪匯向大家總結(jié)和對(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()}

          運(yùn)行代碼如下,結(jié)果顯示temp變量被分配在棧上并沒(méi)有分配在堆上:


          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

          當(dāng)我們把上述代碼更改:


          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é)

           


          JavaGo
          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為什么可以這么牛?

          Serverless 在大廠都怎么用?





          瀏覽 73
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费毛片十八 | 日韩黄片在线看 | 日本A V中文字幕 | 69**操逼| 亚洲精品电影网 |