最簡單的C++/Java程序
最簡單的Java程序:
class Program
{
public static void main()
{
new int;
}
}
對應的C++程序:
void main()
{
new int;
}
我想沒有一個Java程序員會認為上面的Java代碼存在問題。但是所有嚴謹的C++程序員則馬上指出:上面這個C++程序有問題,它存在內存洩漏。但是我今天想和大家交流的一個觀念是:這個C++程序沒有什麼問題。
DocX程序的內存管理
DocX是我開發的一個文檔撰寫工具。這裡有關於它的一些介紹。在這一小節裡,我要談談我在DocX中嘗試的另類內存管理方法。
DocX的總體流程是:
讀入一個C++源代碼(或頭)文件(.h/.c/.hpp/.cpp等),分析其中的注釋,提取並生成xml文檔。
通過xslt變換,將xml文檔轉換為htm。
分析源代碼中的所有include指令,取得相應的頭文件路徑,如果某個頭文件沒有分析過,跳到1反復這些步驟。
最後所有生成的htm打包生成chm文件。
一開始,我象Java/C#程序員做的那樣,我的代碼中所有的new均不考慮delete。當然,它一直運作得很好,直到有一天我的文檔累計到了一定程度後。正如我們預見的那樣,DocX程序運行崩潰了。
那麼,怎麼辦呢?找到所有需要delete的地方,補上delete?
這其實並不需要。在前面,我給大家介紹了AutoFreeAlloc(參見《C++內存管理變革(2):最袖珍的垃圾回收器》),也許有人在嘀咕,這樣一個內存分配器到底有何作用。——那麼,現在你馬上可以看到它的典型用法之一了:
對於我們的DocX崩潰後,我只是做了以下改動:
加一個全局變量:std::AutoFreeAlloc alloc;
所有的new Type(arg1, arg2, …, argn),改為STD_NEW(alloc, Type)(arg1, arg2, …, argn);
所有的new Type[n],改為STD_NEW_ARRAY(alloc, Type, n);
每處理完一個源代碼文件時,調用一次alloc.clear();
搞定,自此之後,DocX再也沒有內存洩漏,也不再有遇到內存不足而崩潰的情形。
只讀DOM模型(或允許少量修改)的建立
在《文本分析的三種典型設計模式》一文中我推薦大家使用DOM模型去進行文件操作。並且通常情況下,這個DOM模型是只讀DOM模型(或允許少量修改)。
對於只讀DOM模型,使用AutoFreeAlloc是極其方便的。整個DOM樹涉及的內存統一由同一個AutoFreeAlloc實例進行分配。大體如下:
class Document;
class ObjectA
{
private:
Document* m_doc;
SubObject* m_c;
public:
ObjectA(Document* doc) : m_doc(doc) {
m_c = STD_NEW(doc->alloc, SubObject);
}
* getC() {
return m_c;
}
};
class Document
{
public:
AutoFreeAlloc alloc;
private:
ObjectA* m_a;
ObjectB* m_b;
public:
ObjectA* getA() {
if (m_a == NULL)
m_a = STD_NEW(alloc, ObjectA)(this);
return m_a;
}
};
通過這種方式創建的DOM模型,只要你刪除了Document對象,整個DOM樹自然就被刪除了。你根本不需要擔心其中有任何內存洩漏的可能。
另類內存管理的觀念
通過以上內容,我試圖向大家闡述的一個觀點是:
有了AutoFreeAlloc後,C++程序員也可以象GC語言的程序員一樣大膽new而不需要顧忌什麼時候delete。
展開來講,可以有以下結論:
如果你程序的空間復雜度為O(1),那麼只new不delete是沒有問題的。
如果你程序的空間復雜度為O(n),並且是簡單的n*O(1),那麼可以用AutoFreeAlloc簡化內存管理。
如果你程序的空間復雜度為O(t),其中t是程序運行時間,並且你不能確定程序執行的總時間,那麼AutoFreeAlloc並不直接適合你。比較典型的例子是Word、Excel等文檔編輯類的程序。
用AutoFreeAlloc實現通用型的GC
AutoFreeAlloc對內存管理的環境進行了簡化,這種簡化環境是常見的。在此環境下,C++程序員獲得了無可比擬的性能優勢。當然,在一般情形下,AutoFreeAlloc並不適用。
那麼,一個通用的半自動GC環境在C++是否可能?《C++內存管理變革》系列的核心就是要告訴你:當然可以。並且,我們推薦C++程序員使用半自動的GC,而不是Java/C# 中的那種GC。
通用的半自動GC環境可以有很多種建立方式。這裡我們簡單聊一下如何使用AutoFreeAlloc去建立。
我們知道,使用AutoFreeAlloc,將導致程序隨著時間推移,逐步地吃掉可用的內存。假設現在已經到達我們設置的臨界點,我們需要開始gc。整個過程和Java等語言的gc其實完全類似:通過一個根對象(Object* root),獲得所有活動著的對象(Active Objects),將它們復制到一個新的AutoFreeAlloc中:
Object* gc(AutoFreeAlloc& oldAlloc, Object* root, AutoFreeAlloc& newAlloc)
{
Object* root2 = root->clone(newAlloc);
oldAlloc.clear();
return root2;
}
如果C++象Java/C#那樣有足夠豐富的元信息,那麼Object::clone過程就可以象Java/C# 等語言那樣自動完成。這些元信息對於GC過程的用處無非在於,我們可以遍歷整個活動對象的集合,然後把這些活動對象復制一份。沒有復制過來的對象自然而然就被丟棄了。
GC的原理就是這麼簡單。沒有元信息也沒關系,只要我們要求每個由GC托管的對象支持clone函數,一切就ok了。對於一個復雜程序,要求每個對象提供clone函數不見得是什麼過分的要求,clone函數也不只有gc過程才需要,很多對象在設計上天然就需要clone。
補充說明
關於全局AutoFreeAlloc變量
我個人非常不推薦使用全局變量(除非是常量:不一定用const修飾,指的是經過一定初始化步驟後就不在修改的變量)。上面只是對於小型的單線程程序偷懶才這樣做。
關於用AutoFreeAlloc實現通用型的GC
請注意我沒有討論過於細節的東西。如果你決定選擇這種做法,請仔細推敲細節。可以預見的一些細節有:
AutoFreeAlloc與線程模型(ThreadModel)。AutoFreeAlloc關注點在於快,它通常不涉及跨線程問題。但是如果要作為通用型的GC,這一點不能不考慮。為了性能,推薦每個線程獨立管理內存,而不要使用互斥體。
性能優化。可以考慮象Java的GC那樣,使用兩個AutoFreeAlloc,把對象劃分為年輕代和年老代。