無論在哪一種系統平臺/編程語言下,內存資源管理是很是重要的問題,稍不留意就會致使內存泄漏,更有甚者訪問非法空間,致使錯誤。說到底,沒有在合適的時機釋放對象,或者訪問了已經釋放的資源。在有垃圾回收的語言中,由平臺環境負責資源的及時回收;在C++中則須要程序員本身把握,在一些多線程狀態下,對象資源的釋放時機經常很差把握,致使了各類各樣的問題。爲何你們喜歡用帶有GC功能的語言作開發,是由於少了太多的心智負擔(JAVA, C#, Python ...)。c++
針對原生指針的管理方式,學校老師經常這麼教:誰建立、誰釋放。要配對,不過現實每每沒那麼理想,在單線程處理程序中這樣沒有問題,可是在多線程中有時候作不到。我如今一般的作法是作一個簿記工做,對於系統中大量使用的對象資源,尤爲是跨線程使用的,會集中管理登記,並配以狀態標記,確認對象使用完畢後再行釋放。對象自身跟隨業務狀態變化,有明確的開始和結束狀態。git
在C++ 中有多種類型的智能指針,有些被重用,而有些卻被放棄,在陳碩的《Linux多線程服務端編程》中,推薦使用shared_ptr以及weak_ptr進行資源管理。此處梳理一下C++中的智能指針。看看各自如何使用,適合在什麼場景下使用。程序員
unique_ptr負責獨佔對應對象的全部權,一旦unique_ptr析構,那麼對應對象自動銷燬,unique_ptr對應對象的控制權能夠轉移,但不可拷貝,具備獨佔性。可經過實例查看。github
class A { public: A() { std::cout << "A construct." << std::endl; a = 10; b = 11; } ~A() { std::cout << "A destruct." << std::endl; } void func() { std::cout << "A::func()" << std::endl; std::cout << a << " " << b << std::endl; } private : int a; int b; }; void unique_ptr_test() { { std::unique_ptr<A> up1(new A()); up1->func(); { std::unique_ptr<A> up2(std::move(up1)); std::cout << "control from up1 to up2." << std::endl; up2->func(); //up1->func(); 此處執行會報錯,up1爲empty up1 = std::move(up2); std::cout << "return control from up2 to up1" << std::endl; up1->func(); } } { A* as = new A[5]; std::unique_ptr<A[]> pas(as); } }
A construct. A::func() 10 11 control from up1 to up2. A::func() 10 11 return control from up2 to up1 A::func() 10 11 A destruct. A construct. A construct. A construct. A construct. A construct. A destruct. A destruct. A destruct. A destruct. A destruct.
這個測試用例主要用來講明幾個問題:算法
shared_ptr和weak_ptr是一對組合。shared_ptr是計數型智能指針,屬於強引用,每個關聯都有計數,而weak_ptr是弱引用,不會影響計數功能。編程
shared_ptr獨立測試數組
void shared_ptr_test() { std::shared_ptr<A> sp1(new A()); std::cout << sp1.use_count() << std::endl; { std::shared_ptr<A> sp2(sp1); std::cout << sp2.use_count() << std::endl; std::shared_ptr<A> sp3(sp2); std::cout << sp3.use_count() << std::endl; { std::shared_ptr<A> sp4(sp3); std::cout << sp4.use_count() << std::endl; } } } A construct. 1 2 3 4 A destruct.
weak_ptr獨立測試 在微軟 MSDN上看到以下一段話:多線程
The template class describes an object that points to a resource that is managed by one or more shared_ptr Class objects. 該模板類用於描述一個對象,該對象已經由一個或者多個shared_ptr對象管理控制。 The weak_ptr objects that point to a resource do not affect the resource's reference count. weak_ptr對象指向一個資源,不會影響該資源的引用計數。 Thus, when the last shared_ptr object that manages that resource is destroyed the resource will be freed, even if there are weak_ptr objects pointing to that resource. This is essential for avoiding cycles in data structures. 當最後一個指向資源的shared_ptr對象析構後,資源被釋放,即便還有weak_ptr指向該資源。該方法經常使用於避免循環引用。 void weak_ptr_test() { { std::shared_ptr<A> sp1(new A()); std::weak_ptr<A> wp1(sp1); std::cout << "shared use count: " << sp1.use_count() << std::endl; std::cout << "weak_ptr use count: " << wp1.use_count() << std::endl; { std::shared_ptr<A> sp2(new A()); std::weak_ptr<A> wp2(sp1); std::cout << "shared use count: " << sp2.use_count() << std::endl; std::cout << "weak_ptr use count: " << wp2.use_count() << std::endl; } } } A construct. shared use count: 1 weak_ptr use count: 1 A construct. shared use count: 1 weak_ptr use count: 1 A destruct. A destruct.
經過示例能夠看到,即便有weak_ptr指向對象,也只顯示shared_ptr強類型智能指針的指向數量。編程語言
weak_ptr的一個做用是能夠有效判斷某個對象是否還存活,示例以下:測試
void shared_weak_ptr_test() { std::weak_ptr<A> wp1; { std::shared_ptr<A> sp1(new A()); wp1 = sp1; std::cout << "shared use count: " << sp1.use_count() << std::endl; std::cout << "weak_ptr use count: " << wp1.use_count() << std::endl; } if (wp1.lock() != nullptr) { std::cout << "resource exists." << std::endl; } else { std::cout << "resource not exists." << std::endl; } } A construct. shared use count: 1 weak_ptr use count: 1 A destruct. resource not exists.
能夠看到,經過weak_ptr進行lock,若是資源存在,那麼能夠轉型爲shared_ptr, 若是資源不存在,那麼返回的就是nullptr,這經常能夠用於多線程程序中判斷某個對象是否有效。
shared_ptr和weak_ptr聯合測試 爲何shared_ptr和weak_ptr老是聯合使用呢,在陳碩《Linux多線程服務端編程》中用了一個很是生動的示例進行說明,就是觀察者模式。當被觀察者發生某個事件,須要通知多個觀察者時,每每是經過指針依次調用。這邊存在的一個問題就是,若是某個觀察者在其餘線程中被刪除,所指對象已經被刪除,那麼在調用方法時,就會出現問題。由於對象已經無效。我本身編寫了一個簡易示例,並未在多線程中運行,可是能夠做爲說明。
class Observer { public: Observer(int32_t id) : observer_id(id) {} void update() { std::cout << observer_id << " Observer " << std::endl; } private: int32_t observer_id; }; class Observable { public: void notifyall() { std::lock_guard<std::mutex> guard(observable_mutex); for(std::vector<std::weak_ptr<Observer>>::iterator it = observers.begin(); it != observers.end(); ) { std::weak_ptr<Observer> ov = *it; std::shared_ptr<Observer> sp(ov.lock()); if (sp != nullptr) { sp->update(); it++; } else { it = observers.erase(it); } } if (observers.size() == 0) { std::cout << "no observers. ." << std::endl; } } void reg(std::weak_ptr<Observer> ob) { std::lock_guard<std::mutex> guard(observable_mutex); observers.push_back(ob); } private: std::vector<std::weak_ptr<Observer>> observers; std::mutex observable_mutex; }; void observer_test() { std::shared_ptr<Observable> bk; { std::shared_ptr<Observer> ob1(new Observer(1)); std::shared_ptr<Observer> ob2(new Observer(2)); std::shared_ptr<Observer> ob3(new Observer(2)); std::shared_ptr<Observable> obed1(new Observable); obed1->reg(ob1); obed1->reg(ob2); obed1->reg(ob3); obed1->notifyall(); bk = obed1; } bk->notifyall(); } 1 Observer 2 Observer 2 Observer no observers. .
經過上面的示例能夠看到,即便註冊的觀察者已經被釋放,被觀察者也可正確識別對象的可用性,而不會執行致使core的錯誤。
cpp還有其餘類型的智能指針,好比auto_ptr,不過目前在c++11中並不被推薦使用。 auto_ptr和unique_ptr有些相似,都是表達對資源的惟一全部權,可是區別是,auto_ptr能夠經過賦值操做默認轉移全部權,而unique_ptr須要顯式的表達轉移動做。
void auto_ptr_test() { { std::auto_ptr<A> ap(new A()); ap->func(); std::auto_ptr<A> ap2 = ap; ap2->func(); ap->func(); //此處會出現錯誤,由於ap已經不擁有A對象的資源,在訪問對象內部變量的時候,天然會報錯 } }
這種經過賦值行爲就實現資源轉移,確實會讓人感到詫異。有一種不告而取的感受。
{ A* a = new A(); std::auto_ptr<A> ap(a); std::auto_ptr<A> ap2(a); }
該段測試代碼會致使資源的重複釋放問題。
第一次據說C++ 中的垃圾回收器,是在以下知乎中的連接看到。
blink中的垃圾回收器 從通常性考慮來說,Cpp能夠很精確的控制內存,也有各類智能指針使用,爲何還要有垃圾回收呢。這不是剝奪了Cpp程序員DEBUG的樂趣麼。文章解釋說由於工程規模大,即便有智能指針等各類技術,仍是避免不了內存泄漏等問題,最後仍是在相應項目下提供一套通用的垃圾回收機制。
源碼中使用了不少模板技術,看不懂。
此處內容主要參考書籍《垃圾回收的算法與實現》,說明JVM下的主要垃圾回收辦法。
JAVA下的垃圾回收並無採用計數法。由於計數沒法解決循環引用的問題。思考下C++中相互引用的兩個類,都擁有對方的shared_ptr類型的指針,如何正確釋放對象。這也是爲啥有weak_ptr的緣由。
JAVA中全部經過new出來的資源對象存放在堆中,相關的引用放在堆棧上。在垃圾回收時,採起可達性分析。經過一系列的「GCRoots」對象做爲起點進行搜索,全部可直接或者間接與GC Roots相連的爲有效對象,其餘則爲無效對象;區分開有效、無效對象後,就可進一步處理。
先說幾個通用算法。
該算法很好理解,可是將各對象放置到空閒鏈表中會致使空間不連續,容易致使內存碎片,一些大塊內存沒法成功申請;同時申請空間會變慢,由於每次遍歷空閒列表尋找合適內存空間都要花費時間;
爲了不內存碎片的問題,複製算法將堆空間一分爲二,當執行回收時,將有效對象複製到另外的空間,而後將原有空間清空便可。可是這樣致使堆空間的使用率大大降低。優勢是若是大量對象須要回收,那麼只須要移動不多一部分存活對象便可完成垃圾回收。
標記整理算法能夠稱的上是標記清除和複製的綜合體,在標記完成後,將全部有效對象都往一端移動,保證堆空間的緊湊。
分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲新生代(Young Generation)和老年代(Tenured Generation)。
新生代特色是每次垃圾回收都有大量的對象須要被回收,只剩下少許有效對象。 老年代的特色是每次垃圾收集只有少許對象須要被回收。 不一樣的特色,可使用不一樣的垃圾回收算法進行處理,從而提升總體的回收效率。
目前大部分垃圾收集器對於新生代採起復制算法,由於新生代中每次垃圾回收都要回收大部分對象,只有少許有效存活對象,只須要複製少許對象便可完成新生代的垃圾回收。可是實際中並非按照1:1的比例來劃分新生代的空間的,而是按照8:1:1,將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另外一塊Survivor空間中,而後清理掉Eden和剛纔使用過的Survivor空間。
因爲老年代的特色是每次回收都只回收少許對象,通常使用的是標記壓縮算法。
另外還有一個代就是永久代(PermanetGeneration),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部份內容:廢棄常量和無用的類。在Oracle JVM中,永久代並不屬於堆空間。
新生代中的S0,S1是輪流使用。這個技巧在不少開發中用到。相似於雙緩衝。
在新生代中的年齡達到必定閾值後,會被轉移到老年代。
後續會進一步深刻研究各回收算法。