問題的發現

這個問題是我在寫C++時考慮到的,C++需要手動管理內存,雖然現在標準庫中提供了一些智能指針,可以實現基於引用計數的自動內存管理,但現實環境是很複雜的,我們仍要注意循環引用的問題。還有一個容易被忽視的問題就是對象間關係的「佔有」和「非佔有」,這個問題其實在具有GC的C#和Java中也一樣存在。

目前.NET和Java的GC策略都屬於Tracing garbage collection,基本原理是從一系列的root開始,沿著引用鏈進行遍歷,對遍歷過的對象進行標記(mark),表示其「可達(reachable)」,然後回收那些沒有標記的,即「不可達」對象所佔用的內存。如果你的代碼中明明有的對象已經沒用了,但在某些地方仍然保持有對它的引用,就會造成這個對象長期處於「可達」狀態,以至其佔用的內存無法被及時回收。

對象關係的問題

佔有 與 非佔有

好吧,這兩個詞是我自己發明的。這兩個詞是針對「擁有」而言的,佔有 是表示強的擁有,宿主對象會影響被擁有對象的生命周期,宿主對象不死,被擁有的對象就不會死;非佔有 表示弱的擁有,宿主對象不影響被擁有對象的生命周期。

在處理對象間關係時,如果應該是非佔有關係,但卻實現成了佔有關係,則佔有關係就會妨礙GC對被佔有對象的回收,輕則造成內存回收的不及時,重則造成內存無法被回收。這裡我用C#實現觀察者模式作為示例:

public interface IPublisher

{

void Subscribe(ISubscriber sub); void UnSubscribe(ISubscriber sub); void Notify();}public interface ISubscriber{ void OnNotify();}public class Subscriber : ISubscriber

{

public String Name { get; set; } public void OnNotify() { Console.WriteLine($"{this.Name} 收到通知"); }}public class Publisher : IPublisher{ private List _subscribers = new List();

public void Notify()

{ foreach (var s in this._subscribers) s.OnNotify(); } public void Subscribe(ISubscriber sub) { this._subscribers.Add(sub); } public void UnSubscribe(ISubscriber sub)

{

this._subscribers.Remove(sub); }}class Program{ static void Main(string[] args) { IPublisher pub = new Publisher(); AttachSubscribers(pub);

pub.Notify();

GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify(); Console.ReadKey(); } static void AttachSubscribers(IPublisher pub) { var sub1 = new Subscriber { Name = "訂閱者 甲" }; var sub2 = new Subscriber { Name = "訂閱者 乙" };

pub.Subscribe(sub1);

pub.Subscribe(sub2); // 這裡其實賦不賦null都一樣,只是為了突出效果 sub1 = null; sub2 = null; }}

這段代碼有什麼問題嗎?

在AttachSubscribers方法里,創建了兩個訂閱者,並進行了訂閱,這裡的兩個訂閱者都是在局部創建的,也並沒有打算在外部引用它們,它們應該在不久的某個時刻被回收了,但是由於同時它們又存在於發布者的訂閱者列表裡,發布者「佔有」了訂閱者,雖然它們都沒用了,但暫時不會被銷毀,如果發布者一直活著,則這些沒用的訂閱者也一直得不到回收,那為什麼不調用UnSubscribe呢?因為在實際中情況可能很複雜,有些時候UnSubscribe調用的時機會很難確定,而且發布者的任務在於登記和通知訂閱者,不應該因此而「佔有」它們,不應干涉它們的死活,所以對於這種情況,可以使用「弱引用」實現「非佔用」。

弱引用

弱引用是一種包裝類型,用於間接訪問被包裝的對象,而又不會產生對此對象的實際引用。所以就不會妨礙被包裝的對象的回收。

給上面的例子加入弱引用:

class Program

{ static void Main(string[] args) { IPublisher pub = new Publisher(); AttachSubscribers(pub); pub.Notify(); GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify();

Console.WriteLine("=============================================");

pub = new WeakPublisher(); AttachSubscribers(pub); pub.Notify(); GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify(); Console.ReadKey(); } static void AttachSubscribers(IPublisher pub)

{

var sub1 = new Subscriber { Name = "訂閱者 甲" }; var sub2 = new Subscriber { Name = "訂閱者 乙" }; pub.Subscribe(sub1); pub.Subscribe(sub2); // 這裡其實賦不賦null都一樣,只是為了突出效果 sub1 = null; sub2 = null; }}public interface IPublisher{ void Subscribe(ISubscriber sub); void UnSubscribe(ISubscriber sub); void Notify();}public interface ISubscriber{ void OnNotify();}public class Subscriber : ISubscriber{ public String Name { get; set; } public void OnNotify() { Console.WriteLine($"{this.Name} 收到通知"); }}public class Publisher : IPublisher{ private List _subscribers = new List(); public void Notify() { foreach (var s in this._subscribers) s.OnNotify(); } public void Subscribe(ISubscriber sub) { this._subscribers.Add(sub); } public void UnSubscribe(ISubscriber sub) { this._subscribers.Remove(sub); }}public class WeakPublisher : IPublisher{ private List> _subscribers = new List>(); public void Notify() { for (var i = 0; i this._subscribers.Count();) { ISubscriber s; if (this._subscribers[i].TryGetTarget(out s)) { s.OnNotify(); ++i; } else this._subscribers.RemoveAt(i); } } public void Subscribe(ISubscriber sub) { this._subscribers.Add(new WeakReference(sub)); } public void UnSubscribe(ISubscriber sub) { for (var i = 0; i this._subscribers.Count(); ++i) { ISubscriber s; if (this._subscribers[i].TryGetTarget(out s) & Object.ReferenceEquals(s, sub)) { this._subscribers.RemoveAt(i); return; } } }}

其實弱引用也不是完美的解決方案,因為限制了API使用者的自由,當然這裡也沒打算實現一個通用的、完美的解決辦法,只是想通過個例子讓你知道,即使是在有GC的情況下,不注意代碼設計的話,仍有可能會發生內存泄漏的問題。

非託管資源

GC不會釋放非託管資源嗎?

GC的作用在於清理託管對象,託管對象是可以定義析構方法(準確點說應該叫finalizer,C#中的~類名,Java中的finalize)的,這個方法會在託管對象被GC回收前被調用,析構方法里完全可以通過調用平台API釋放非託管資源(實際上很多託管對象的實現也都這麼做了),也就是說GC是可以釋放非託管資源的。以下代碼摘自.NET類庫中FileStream:

[System.Security.SecuritySafeCritical] // auto-generated

~FileStream(){ if (_handle != null) { BCLDebug.Correctness(_handle.IsClosed, "You didnt close a FileStream & it got finalized. Name: ""+_fileName+"""); Dispose(false); }}[System.Security.SecuritySafeCritical] // auto-generatedprotected override void Dispose(bool disposing){ // Nothing will be done differently based on whether we are // disposing vs. finalizing. This is taking advantage of the // weak ordering between normal finalizable objects & critical // finalizable objects, which I included in the SafeHandle // design for FileStream, which would often "just work" when // finalized. try { if (_handle != null && !_handle.IsClosed) { // Flush data to disk iff we were writing. After // thinking about this, we also dont need to flush // our read position, regardless of whether the handle // was exposed to the user. They probably would NOT // want us to do this. if (_writePos > 0) { FlushWrite(!disposing); } } } finally { if (_handle != null & !_handle.IsClosed) _handle.Dispose(); _canRead = false; _canWrite = false; _canSeek = false; // Dont set the buffer to null, to avoid a NullReferenceException // when users have a race condition in their code (ie, they call // Close when calling another method on Stream like Read). //_buffer = null; base.Dispose(disposing); }}

可以看到FileStream的析構方法里調用了Dispose,繼而調用了_handle.Dispose,_handle.Dispose內部調用的可能是一些native api(一般是用C實現的)。

但是如果託管對象的生命很長,甚至比如說它的靜態的,則它內部包裝的資源將一直得不到回收,而且託管對象內部包裝資源可能屬於「緊張的資源」,比如非託管內存、文件句柄、socket連接,這些資源是必須要被及時回收的,比如文件句柄不及時釋放會導致該文件一直被佔用,影響其它進程對該文件的讀寫、socket連接不及時釋放會導致埠號一直被佔用,為了解決這些問題,我們需要顯式地去釋放這些資源。

Dispose模式

一個常見的做法就是在對象中定義一個方法來專門釋放這些非託管資源,比如叫close, dispose, free, release之類,然後在不需要使用此對象時顯式調用這個方法。C#中的IDisposable介面和Java中的Closeable介面就是這個作用,因為大多數帶GC的語言都使用這種設計,所以這也算是一種模式。

偽代碼示例:

File f = File.openWrite("data.txt");

f.writeBytes((new String("Hello, world!")).getBytes("ascii"));f.close();

這樣就夠了嗎?如果close前發生異常或直接return了怎麼辦? — finally語句塊

finally語句塊保證了其中的語句一定會被執行,配合close方法,就能確保非託管資源的釋放。

C++中沒有finally語句結構,這並不奇怪,因為C++有RAII機制,對象的銷毀是確定的,而且確保析構函數的調用,所以不需要finally這種語法。

文章轉載,希望對大家有幫助!


推薦閱讀:
相关文章