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