不同內(nèi)存管理方式的聰明程度大 PK
代碼要在計(jì)算機(jī)上跑起來,需要一系列計(jì)算機(jī)資源:內(nèi)存、網(wǎng)絡(luò)端口、打開的文件等等,這些資源一起被叫做進(jìn)程。
進(jìn)程有一個(gè)專門的控制塊來記錄這些資源,叫做進(jìn)程控制塊(PCB)。
這些資源里面最重要的就是內(nèi)存了,進(jìn)程啟動(dòng)的時(shí)候會(huì)向操作系統(tǒng)申請(qǐng)一些內(nèi)存。
如果內(nèi)存是無限的,那么我們?cè)谏厦娣艛?shù)據(jù)、代碼等,不用擔(dān)心不夠用,但可惜內(nèi)存是有限的,我們要把用不到的內(nèi)存及時(shí)的回收掉,用來放別的東西,這樣代碼才能正常的運(yùn)行。
內(nèi)存分為代碼區(qū)、全局?jǐn)?shù)據(jù)區(qū)、堆區(qū)、棧區(qū)等,這是操作系統(tǒng)可執(zhí)行文件的內(nèi)存模型,如果是 javascript、java 這種解釋型語(yǔ)言,那還會(huì)再做自己的一些劃分。但總體來說,都是分為這幾部分。
代碼區(qū)的內(nèi)容基本不變。
棧區(qū)存放隨著函數(shù)調(diào)用而聲明的局部變量,每個(gè)函數(shù)一個(gè)棧幀,它是有上限的,調(diào)用層次過深會(huì)棧溢出。
全局?jǐn)?shù)據(jù)區(qū)存放全局變量。
棧區(qū)和全局?jǐn)?shù)據(jù)區(qū)中的大對(duì)象會(huì)存放在堆上,只留一個(gè)引用。
堆區(qū)存放動(dòng)態(tài)分配的大對(duì)象,占內(nèi)存最多,我們內(nèi)存管理也主要是管理堆內(nèi)存。
為了管理好這一畝三分地的堆內(nèi)存,不同的語(yǔ)言有不同的方式,聰明程度各不相同,我們來看一下誰(shuí)更聰明吧:
C、C++
C、C++ 的內(nèi)存都是程序員手動(dòng)管理的,比如 C++ 的 class 有構(gòu)造函數(shù)和析構(gòu)函數(shù),構(gòu)造函數(shù)里申請(qǐng)內(nèi)存,析構(gòu)函數(shù)里面就把這些內(nèi)存釋放掉。
是否漏掉一些內(nèi)存沒釋放取決于程序員,很看程序員水平。
騰訊之前是大規(guī)模用 C++ 做服務(wù)端開發(fā)的,但是后來也逐漸轉(zhuǎn)向 go、java 了,因?yàn)?C++ 這種手動(dòng)管理內(nèi)存的方式,萬(wàn)一某個(gè)程序員漏掉了一些內(nèi)存沒釋放,那就內(nèi)存泄漏了。(內(nèi)存泄漏就是不再使用的內(nèi)存一直占用著,導(dǎo)致可用內(nèi)存減少),而服務(wù)器是長(zhǎng)時(shí)間跑的,輕微的內(nèi)存泄漏逐漸積累最終都會(huì)導(dǎo)致進(jìn)程崩潰。
靠程序員來保證釋放掉不用的內(nèi)存太難了,如果程序能自己回收這些垃圾內(nèi)存就好了,那就解放了程序員了,代碼可靠性也更高。所以后來的高級(jí)語(yǔ)言基本都有了自動(dòng)的垃圾回收機(jī)制。
java、javascript
c++ 那種手動(dòng)管理內(nèi)存的方式太麻煩了,所以 java 和 javascript 設(shè)計(jì)之初就不讓程序員操作內(nèi)存,而是自己做了一套垃圾回收機(jī)制,定期把沒用的內(nèi)存釋放下。
怎么檢測(cè)哪些內(nèi)存沒用呢?最開始的思路是對(duì)每個(gè)對(duì)象都記錄下引用數(shù),如果沒有被引用了,那就可以回收了,這種思路叫引用計(jì)數(shù)。
但是這個(gè)思路有個(gè)問題,萬(wàn)一兩個(gè)對(duì)象你引用我我引用你,并且都沒被別的對(duì)象引用,這種循環(huán)引用的問題檢查不出來。
看來這種方式還不夠聰明。怎么優(yōu)化呢?
從全局的對(duì)象開始,把所有引用的對(duì)象標(biāo)記一遍,沒被標(biāo)記的就清掉。這樣不管是沒被引用的,還是循環(huán)引用但是都沒被別的對(duì)象引用的,都可以檢查出來,這種思路叫做標(biāo)記清除。
標(biāo)記清除的思路更聰明些,所以現(xiàn)在的 js 引擎基本都用這個(gè)思路。
這樣的內(nèi)存管理思路其實(shí)也是存在問題的,萬(wàn)一有的不用的對(duì)象被放到全局了,那就永遠(yuǎn)不會(huì)回收了。這種也會(huì)內(nèi)存泄漏。
這個(gè)只能靠程序員排查了,通過工具把一些不該放到全局的變量給找出來。
js 的內(nèi)存泄漏排查一般都是用 chrome devtools 的 memory 工具,他可以取到某個(gè)時(shí)間點(diǎn)的內(nèi)存快照,做一些操作后,再取一次內(nèi)存快照,兩個(gè)內(nèi)存快照對(duì)比下就能找出增加了哪些全局變量。然后定位到那段內(nèi)存泄漏的代碼。
比如這樣一段代碼:
5s 后在全局聲明一個(gè)變量 aaa,是正則表達(dá)式類型。
我們用 chrome devtools 的 memory 工具分別取兩次快照。
這里有不同的視圖,我們選擇比較視圖來對(duì)比兩個(gè)快照:
可以看到 delta 那一列,顯示了正則表達(dá)式的對(duì)象 + 1,這就是我們定時(shí)器里聲明的那個(gè)全局變量。
通過這種內(nèi)存快照的對(duì)比,就可以定位什么操作導(dǎo)致的內(nèi)存泄漏,進(jìn)而定位到代碼。
自動(dòng)的垃圾回收避免了程序員沒有釋放一些內(nèi)存導(dǎo)致的泄漏,但是仍然會(huì)有把沒用的對(duì)象放到全局導(dǎo)致的泄漏。這種方案比較聰明,但也是有問題的。
rust
rust 也不需要程序員手動(dòng)管理內(nèi)存,但也沒有垃圾回收,卻把內(nèi)存管理的更好,而且能避免 99% 的內(nèi)存泄漏問題。它是怎么做到的呢?
rust 覺得堆中的對(duì)象之所以難管理就是因?yàn)楸惶嗟胤揭昧?,如果限制了?duì)象只能屬于某個(gè)函數(shù),只能有一個(gè)引用,別的引用自己復(fù)制一份去,這樣函數(shù)調(diào)用結(jié)束就可以把用到的堆中的對(duì)象全部回收了,根本不會(huì)留下垃圾。這種思路叫做所有權(quán)機(jī)制。
所有權(quán)機(jī)制通過限制對(duì)象的引用的方式來做到了不需要垃圾回收器也能很好的管理內(nèi)存。而且也沒有 js 那種不小心把對(duì)象放到全局就會(huì)內(nèi)存泄漏的問題。
rust 的所有權(quán)機(jī)制是更聰明的一種內(nèi)存管理方式,也是因?yàn)檫@個(gè)原因,rust 正變得越來越火。
總結(jié)
進(jìn)程的可用內(nèi)存是有限的,需要及時(shí)把不再用到的變量的內(nèi)存釋放掉,不同語(yǔ)言對(duì)內(nèi)存管理的方式不同,聰明程度不同:
c、c++ 是靠程序員自己管理內(nèi)存的,萬(wàn)一不小心某個(gè)內(nèi)存沒釋放就泄漏了。
java、javascript 則是不讓程序員自己管理,有專門的垃圾回收器,最開始通過引用計(jì)數(shù),后來改成了標(biāo)記清除,通過這種方式來找到?jīng)]用的內(nèi)存釋放掉。
但萬(wàn)一把沒用的對(duì)象放到了全局,那就回收不了了,這種就是內(nèi)存泄漏,需要用 chrome devtools 的 memory 工具記錄兩次快照,然后做 diff,通過看內(nèi)存是否增加來定位到導(dǎo)致內(nèi)存泄漏的代碼。
rust 也不用程序員手動(dòng)管理內(nèi)存,但也沒有垃圾回收器,它限制了對(duì)象只能有一個(gè)引用,這樣函數(shù)調(diào)用結(jié)束就可以把對(duì)象回收掉,根本不會(huì)留下垃圾,而且也避免了把沒用的對(duì)象放到全局的那種內(nèi)存泄漏(因?yàn)橹辉试S一個(gè)引用)。
語(yǔ)言的發(fā)展規(guī)律就是這樣,讓程序員做的事情更少,也讓程序的健壯性更高。這需要更聰明的語(yǔ)言設(shè)計(jì),更強(qiáng)大的編譯器/解釋器。