GC是錯誤的內存管理模式Posted on 27 Jul 2013
只要寫程序, 就免不了要和各種資源打交道, 其中最頻繁的莫過于內存了. 任何一個程序都需要內存管理. 它不管是簡單的還是復雜的, C 語言的還是 java 語言的. 所不一樣的是, 內存管理的細節(jié)掌握在誰的手里. 對于 C 語言, 毫無疑問的, 程序員掌握所有細節(jié). 程序員獲得了最大的靈活性, 作為代價, 編譯器不對任何內存管理上的疏忽負責. 而人是 最容易犯錯 的生物. 意味著, 程序員總會犯錯, 因此很難寫出在內存管理上沒有瑕疵的程序. 毫無疑問的, 將內存管理的重擔全部丟給程序員, 是編譯器水平低下的時代無奈的選擇. 隨著編譯器技術的發(fā)展, 將內存管理的任務從程序員手中接管是必然的. 對于如何接管內存管理, 語言作者們分成了截然不同的兩派
在帶有垃圾收集的語言里, 程序員只管分配內存, 無需操心釋放. 垃圾收集器間歇性的運作, 會將不再使用的內存釋放掉. 至于如何標記哪些內存是不再使用的, 幾十年間發(fā)展出了各種算法. 許多語言都帶有多種標記算法供選擇. "沒有哪一種垃圾收集策略是適合所有程序的, 所以各種語言都發(fā)展出多套垃圾收集器, 供運行時選擇."
垃圾收集不是完美的, 使用垃圾收集并不意味著就可以高枕無憂了. 垃圾收集并不意味著內存泄漏成為過去式, 倒是野指針確實成為了過去式, 因為只要還有指針引用一個對象, 這個對象就絕對不會被釋放. (不過, 帶有垃圾收集的語言或多或少都廢除了指針吧, 用引用替代了指針) 有很多很多復雜的原因丟會導致垃圾收集器無法回收特定的內存, 導致這部分內存泄漏. 更嚴重的是, 你很難將內存泄漏和還未被清除的內存完全區(qū)別開來. 到底是延遲收集策略 還是真的發(fā)生了內存泄漏 ? 你永遠都無法正確分辨. 結果是, 程序員最終不得不回到 C 語言的老路上, 小心的檢查所有的內存分配, 確保沒有觸發(fā)垃圾收集器的bug或者特定的一些策略 . 幾乎所有使用帶垃圾收集的語言開發(fā)的程序, 在其開發(fā)的后期都要經歷慘痛的 "內存檢查" , 回顧所有可能導致內存泄漏的代碼. 垃圾收集器的另一個問題是, 除了內存, 它無法對程序使用的其他資源執(zhí)行垃圾收集. 垃圾收集是以內存管理為目標產生的, 只能收集不再使用的內存, 而不能收集程序使用的其他資源, 如消息列隊, 文件描述符, 共享內存, 鎖.等等. 程序員不得不對其他資源執(zhí)行手工管理, 像 C 程序員那樣小心翼翼的操作. 最終垃圾收集仍然沒有解決 "人容易犯錯" 的問題, 還是把其他資源的泄漏問題丟給了程序員.C++ 從來不認為垃圾收集是有用的東西, 和 C 派不一樣 , C 派不喜歡垃圾收集純粹是因為喜歡 "自己控制一切" (天生的 M 屬性). C++ 派同樣認為, 要把程序員從資源管理的重擔里解放出來. 同 "投機取巧" 的 GC派不同, C++ 做了很多思考, 并最終經歷了 30年的時間終于找到了解決的辦法. 寫入了 C++11 標準. 在這30年的時間里, C++ 的資源管理是逐步發(fā)展的. C++11 最終提出的智能指針, 源于 C++30年的探索. C++ 要想實現 RAII + 智能指針, 兩大技術缺一不可 1. 自動確定并調用 構造函數和析構函數 2. 模板 C++ 的第一步試圖解放資源管理重任, 是為 C 加入了構造函數和析構函數. 構造函數和析構函數由編譯器調用, 生命期終止的對象會自動調用析構函數. 不管生命期終止的原因是 return 返回, 還是 拋出了異常, 編譯器總是保證, 生命期終止的對象一定會被調用析構函數. 以 "編譯器自動保證對象生命期" 的技術依托下, C++ 發(fā)明了 RAII 技術, 將資源的管理變成了對象的管理,而自動變量 (創(chuàng)建在棧上的對象, 類的成員變量) 的生命期由編譯器自動保證, 只要在構造函數里申請資源, 在析構函數里正確的釋放資源, RAII 技術解決了一大部分的資源管理問題. 模板的引入使得 RAII 技術得以 "一次實現到處使用". 如實現一次 std::vector 就可以到處使用在需要數組的情況下, 而無需為每種類型的分別實現數組RAII類. STL 內置了大量的容器, 幾乎滿足了所有的需求. STL的容器無法滿足需求的情況下, 程序員仍然能借用 STL 的理念實現自己的 RAII 容器. 但是, 如果對象分配于堆上, 程序員不得不手工調用 delete 刪除對象. 忘記將不用的對象 delete 就成了頭號資源泄漏原因. 如果指針也是自動對象就好了. C++ 標準的第一次嘗試是納入 std::autoptr . 但是效果并不好, 不是所有指針都可以為 autoptr 所代替. 最要命的是, STL 容器無法使用 auto_ptr. C++ 標準的第二次嘗試就是納入了 std::sharedptr , sharedptr 在進入 C++11 標準之前, 已經在 Boost 庫里實踐了相當長的時間. 首先得益于 C++ 的模板技術, shared_ptr 只需實現一次, 即變成可用于任何類型的指針. 其次, 得益于 C++ 的自動生命期管理, 智能指針將需要程序員管理的堆對象也變成了能利用編譯器自動管理的自動變量. 也就是, 智能指針徹底的將 delete 關鍵字 變成了 sharedptr 才能使用的內部技術. **編譯器能自動刪除 sharedptr 對象, 也就是編譯器能自動的發(fā)出 delete 調用.** 模板是智能指針技術必不可少的一部分, 否則要利用 RAII 實現智能指針就只能利用 "單根繼承" 這一老土辦法了. 沒錯, 這也是 MFC 使用的. ( MFC 誕生在 模板還沒有加入 C++ 的年代. ) 直到 1998 年 C++ 標準納入了模板, C++ 才最終具備了實現自動內存管理所必須的特性.但是準備好這些特性, 到利用這些特性發(fā)明出真正能用的智能指針, 則又花了13年的時間. ( 2011年加入了 shared_ptr. )發(fā)明出編譯器實現的自動內存管理需要時間, C++ 花了 30年的時間. 沒有耐心的語言走了捷徑, GC 就是這條捷徑. |
|