問(wèn)題發(fā)現(xiàn)##
這個(gè)問(wèn)題是我在寫(xiě)C++時(shí)考慮到的啊犬,C++需要手動(dòng)管理內(nèi)存灼擂,雖然現(xiàn)在標(biāo)準(zhǔn)庫(kù)中提供了一些智能指針,可以實(shí)現(xiàn)基于引用計(jì)數(shù)的自動(dòng)內(nèi)存管理觉至,但現(xiàn)實(shí)環(huán)境是很復(fù)雜的剔应,我們?nèi)砸⒁庋h(huán)引用的問(wèn)題。還有一個(gè)容易被忽視的問(wèn)題就是對(duì)象間關(guān)系的“占有”和“非占有”,這個(gè)問(wèn)題其實(shí)在具有GC的C#和Java中也一樣存在峻贮。
目前.NET和Java的GC策略都屬于Tracing garbage collection席怪,基本原理是從一系列的root開(kāi)始,沿著引用鏈進(jìn)行遍歷纤控,對(duì)遍歷過(guò)的對(duì)象進(jìn)行標(biāo)記(mark)挂捻,表示其“可達(dá)(reachable)”,然后回收那些沒(méi)有標(biāo)記的船万,即“不可達(dá)”對(duì)象所占用的內(nèi)存刻撒。如果你的代碼中明明有的對(duì)象已經(jīng)沒(méi)用了,但在某些地方仍然保持有對(duì)它的引用唬涧,就會(huì)造成這個(gè)對(duì)象長(zhǎng)期處于“可達(dá)”狀態(tài)疫赎,以至其占用的內(nèi)存無(wú)法被及時(shí)回收。
對(duì)象關(guān)系的問(wèn)題##
占有與非占有###
好吧碎节,這兩個(gè)詞是我自己發(fā)明的捧搞。這兩個(gè)詞是針對(duì)“擁有”而言的,占有 是表示強(qiáng)的擁有狮荔,宿主對(duì)象會(huì)影響被擁有對(duì)象的生命周期胎撇,宿主對(duì)象不死,被擁有的對(duì)象就不會(huì)死殖氏;非占有 表示弱的擁有晚树,宿主對(duì)象不影響被擁有對(duì)象的生命周期。
在處理對(duì)象間關(guān)系時(shí)雅采,如果應(yīng)該是非占有關(guān)系爵憎,但卻實(shí)現(xiàn)成了占有關(guān)系,則占有關(guān)系就會(huì)妨礙GC對(duì)被占有對(duì)象的回收婚瓜,輕則造成內(nèi)存回收的不及時(shí)宝鼓,重則造成內(nèi)存無(wú)法被回收。這里我用C#實(shí)現(xiàn)觀(guān)察者模式作為示例:
<pre>
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("垃圾回收結(jié)束");
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);
// 這里其實(shí)賦不賦null都一樣巴刻,只是為了突出效果
sub1 = null;
sub2 = null;
}
}
</pre>
<strong>這段代碼有什么問(wèn)題嗎愚铡?</strong>在A(yíng)ttachSubscribers方法里,創(chuàng)建了兩個(gè)訂閱者胡陪,并進(jìn)行了訂閱沥寥,這里的兩個(gè)訂閱者都是在局部創(chuàng)建的,也并沒(méi)有打算在外部引用它們柠座,它們應(yīng)該在不久的某個(gè)時(shí)刻被回收了邑雅,但是由于同時(shí)它們又存在于發(fā)布者的訂閱者列表里,發(fā)布者“占有”了訂閱者愚隧,雖然它們都沒(méi)用了蒂阱,但暫時(shí)不會(huì)被銷(xiāo)毀锻全,如果發(fā)布者一直活著,則這些沒(méi)用的訂閱者也一直得不到回收录煤,那為什么不調(diào)用UnSubscribe呢鳄厌?因?yàn)樵趯?shí)際中情況可能很復(fù)雜,有些時(shí)候UnSubscribe調(diào)用的時(shí)機(jī)會(huì)很難確定妈踊,而且發(fā)布者的任務(wù)在于登記和通知訂閱者了嚎,不應(yīng)該因此而“占有”它們,不應(yīng)干涉它們的死活廊营,所以對(duì)于這種情況歪泳,可以使用“弱引用”實(shí)現(xiàn)“非占用”啃匿。
弱引用###
弱引用是一種包裝類(lèi)型沸呐,用于間接訪(fǎng)問(wèn)被包裝的對(duì)象,而又不會(huì)產(chǎn)生對(duì)此對(duì)象的實(shí)際引用瘟檩。所以就不會(huì)妨礙被包裝的對(duì)象的回收慎式。
給上面的例子加入弱引用:
<pre>
class Program
{
static void Main(string[] args)
{
IPublisher pub = new Publisher();
AttachSubscribers(pub);
pub.Notify();
GC.Collect();
Console.WriteLine("垃圾回收結(jié)束");
pub.Notify();
Console.WriteLine("=============================================");
pub = new WeakPublisher();
AttachSubscribers(pub);
pub.Notify();
GC.Collect();
Console.WriteLine("垃圾回收結(jié)束");
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);
// 這里其實(shí)賦不賦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;
}
}
}
}
</pre>
其實(shí)弱引用也不是完美的解決方案,因?yàn)橄拗屏薃PI使用者的自由瘪吏,當(dāng)然這里也沒(méi)打算實(shí)現(xiàn)一個(gè)通用的癣防、完美的解決辦法,只是想通過(guò)個(gè)例子讓你知道掌眠,即使是在有GC的情況下蕾盯,不注意代碼設(shè)計(jì)的話(huà),仍有可能會(huì)發(fā)生內(nèi)存泄漏的問(wèn)題蓝丙。
非托管資源##
GC不會(huì)釋放非托管資源嗎级遭?###
GC的作用在于清理托管對(duì)象,托管對(duì)象是可以定義析構(gòu)方法(準(zhǔn)確點(diǎn)說(shuō)應(yīng)該叫finalizer渺尘,C#中的~類(lèi)名装畅,Java中的finalize)的,這個(gè)方法會(huì)在托管對(duì)象被GC回收前被調(diào)用沧烈,析構(gòu)方法里完全可以通過(guò)調(diào)用平臺(tái)API釋放非托管資源(實(shí)際上很多托管對(duì)象的實(shí)現(xiàn)也都這么做了),也就是說(shuō)GC是可以釋放非托管資源的像云。以下代碼摘自.NET類(lèi)庫(kù)中FileStream:
<pre>
[System.Security.SecuritySafeCritical] // auto-generated
~FileStream()
{
if (_handle != null) {
BCLDebug.Correctness(_handle.IsClosed, "You didn't close a FileStream & it got finalized. Name: ""+_fileName+""");
Dispose(false);
}
}
[System.Security.SecuritySafeCritical] // auto-generated
protected 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 don't 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;
// Don't 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);
}
}
</pre>
可以看到FileStream的析構(gòu)方法里調(diào)用了Dispose锌雀,繼而調(diào)用了_handle.Dispose,_handle.Dispose內(nèi)部調(diào)用的可能是一些native api(一般是用C實(shí)現(xiàn)的)迅诬。
但是如果托管對(duì)象的生命很長(zhǎng)腋逆,甚至比如說(shuō)它的靜態(tài)的,則它內(nèi)部包裝的資源將一直得不到回收侈贷,而且托管對(duì)象內(nèi)部包裝資源可能屬于“緊張的資源”惩歉,比如非托管內(nèi)存、文件句柄、socket連接撑蚌,這些資源是必須要被及時(shí)回收的上遥,比如文件句柄不及時(shí)釋放會(huì)導(dǎo)致該文件一直被占用,影響其它進(jìn)程對(duì)該文件的讀寫(xiě)争涌、socket連接不及時(shí)釋放會(huì)導(dǎo)致端口號(hào)一直被占用粉楚,為了解決這些問(wèn)題,我們需要顯式地去釋放這些資源亮垫。
Dispose模式###
一個(gè)常見(jiàn)的做法就是在對(duì)象中定義一個(gè)方法來(lái)專(zhuān)門(mén)釋放這些非托管資源模软,比如叫close, dispose, free, release之類(lèi),然后在不需要使用此對(duì)象時(shí)顯式調(diào)用這個(gè)方法饮潦。C#中的IDisposable接口和Java中的Closeable接口就是這個(gè)作用燃异,因?yàn)榇蠖鄶?shù)帶GC的語(yǔ)言都使用這種設(shè)計(jì),所以這也算是一種模式继蜡。
偽代碼示例:
<pre>
File f = File.openWrite("data.txt");
f.writeBytes((new String("Hello, world!")).getBytes("ascii"));
f.close();
</pre>
這樣就夠了嗎回俐?如果close前發(fā)生異常或直接return了怎么辦壹瘟? — finally語(yǔ)句塊
finally語(yǔ)句塊保證了其中的語(yǔ)句一定會(huì)被執(zhí)行鲫剿,配合close方法,就能確保非托管資源的釋放稻轨。
C++中沒(méi)有finally語(yǔ)句結(jié)構(gòu)灵莲,這并不奇怪,因?yàn)镃++有RAII機(jī)制殴俱,對(duì)象的銷(xiāo)毀是確定的政冻,而且確保析構(gòu)函數(shù)的調(diào)用,所以不需要finally這種語(yǔ)法线欲。