內存管理問題
內存管理是編程過程中的一個經典問題,早期在 C 語言時代,幾乎都靠 malloc/free 手動管理內存。隨着各個平台的發展,到現在被廣泛采用的主要有兩個方法:
- 引用計數 (ARC,Automatic Reference Counting)
- GC (Garbage Collection)
管理方法 ARC/GC
因為 Java 的流行,GC 被廣泛的認知。GC 簡單的說是定期查找不再使用的對象,釋放對象占用的內存。
基於 GC,申請的對象不需要手動釋放,只需要確認對象在不再需要時,不再被其他對象引用。
引用計數早期主要用於底層系統,比如文件系統的 inode 管理,后來 C++ 的 boost 庫實現了一套完整的 ARC,目前流行的系統還有 Objective C 也是采用的 ARC。
ARC 的特點是,一個對象被引用時,引用計數增加 1,引用對象釋放時,引用計數減少 1,如果引用計數為 0,釋放對象。
比較
因為 ARC 和 GC 的不同策略,對編程的幾個方面的影響。
性能
GC 需要一套額外的系統跟蹤分配的內存,分析哪些內存需要釋放,相對來說就需要更多的計算。這也是為什么對性能敏感的場景不采用 GC 的原因,比如,高性能的服務端程序,資源有限的嵌入式設備(iOS 就沒有采用 GC)。
ARC 由開發者自己來管理資源在什么時候釋放,不需要額外的資源,所以性能沒有損失。
延遲
GC 回收內存時,需要完全暫停當前程序,這會給程序帶來難以預測的一個延遲期。如果需要回收的資源很多,這個延遲可能會非常大。
ARC 在資源引用為 0 時立即釋放,沒有不可預測的延遲。
編程難度
不難看出,GC 在性能、延遲等方面有明顯的缺點,為什么 GC 還會被廣泛采用呢?
GC 帶來的最大好處是不需要開發者手動管理內存分配,這大大降低了編程難度,同時可以大幅減少跟內存管理相關的 Bug:
- 懸空指針。指針指向的內存被其他代碼釋放
- 重復釋放內存
- 內存泄漏。申請的內存沒釋放
不過使用 GC 並不代表可以完全不用理解內存管理,如果對象的引用關系跟想象的不一致,GC 也會有內存泄漏的問題
。
我們之前理解的內存泄漏
是指一個分配的內存沒有被釋放造成的。而 GC 平台下的內存泄漏是指對象有引用而開發者不知道
,比如:
ObjectA -> ObjectB
ObjectB 使用完后,我們沒有及時把 ObjectA 引用 ObjectB 的指針設置為 NULL,這時, ObjectB 不會被 GC 回收。
- 對比表格
時機 | 性能 | 延遲 | 編程難度 | |
ARC | 引用計數為 0 馬上回收 | 快 | 小 | 較大 |
GC | 定時掃描清理 | 慢 | 大 | 較小 |
怎么選擇 ARC or GC
開發一個項目時,采用什么樣的平台,跟實際面對的場景有很大關系,沒有一個技術是用來解決所有問題的。
一般來說,對延遲和性能不敏感的系統,可以考慮帶 GC 的平台,比如 Java、Go 等來開發,通常可以提高開發效率。
如果需要對系統的性能有良好的控制,或者平台的資源有限,ARC 是更好的選擇。比如操作系統、數據庫等選擇 C 或者 C++。比如 iOS 的 Object C 就是采用 ARC,實際來看比使用 Java (GC) 的 Android 平台的表現要好太多。
但是 ARC 平台一般對開發者要求要更高。
最近出現的新語言 Rust
采用的是 ARC,但是 Rust 會在代碼編譯階段對內存、指針的使用做嚴格的分析和檢查,確保程序沒有內存管理問題。相當於把 GC 的一部分工作移到編譯階段,這樣程序的運行性能幾乎沒有損失,同時又大大減少內存管理相關的 Bug。
我的觀察從 C++11
正式吸納 boost
的 smart pointer
后,C++ 在內存管理方面比之前有極大的提升,如果嚴格的按照 smart pointer 的規范,同樣可以減少內存管理的風險。Rust 就有點像一個嚴格的 C++11 編譯系統。
支持 GC 的平台里面有一個特殊的,就是 Erlang
。Erlang 的 GC 是進程級別的(Erlang 的輕量級進程),意味着 GC 發生時,只暫停當前進程,其他進程不受影響。另外,Erlang 程序往往會運行海量的進程,相當於把 GC 分散開了,所以 Erlang 的 GC 一般不會產生明顯的延遲。
了解這些細節,在面對具體問題時,能幫你做出正確的選擇。
歡迎來微博留下您的意見:http://weibo.com/2255454164/E48lBE9pU
weibo: @Tiger_張虎, 雲巴 (https://yunba.io) 創始人,yunba.io 雲端實時消息服務。 JPush 創始人,原CTO。 Oracle VM 創始團隊成員。