[轉(zhuǎn)]有了垃圾回收晾腔,還會(huì)不會(huì)發(fā)生內(nèi)存泄漏?

問(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ǔ)法线欲。

原文地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末明场,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子李丰,更是在濱河造成了極大的恐慌苦锨,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趴泌,死亡現(xiàn)場(chǎng)離奇詭異舟舒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)嗜憔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)秃励,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人吉捶,你說(shuō)我怎么就攤上這事夺鲜〗远” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵币励,是天一觀(guān)的道長(zhǎng)慷蠕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)榄审,這世上最難降的妖魔是什么砌们? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮搁进,結(jié)果婚禮上浪感,老公的妹妹穿的比我還像新娘。我一直安慰自己饼问,他們只是感情好影兽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著莱革,像睡著了一般峻堰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盅视,一...
    開(kāi)封第一講書(shū)人閱讀 50,096評(píng)論 1 291
  • 那天捐名,我揣著相機(jī)與錄音,去河邊找鬼闹击。 笑死镶蹋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赏半。 我是一名探鬼主播贺归,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼断箫!你這毒婦竟也來(lái)了拂酣?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仲义,失蹤者是張志新(化名)和其女友劉穎婶熬,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體埃撵,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尸诽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盯另。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洲赵,死狀恐怖鸳惯,靈堂內(nèi)的尸體忽然破棺而出商蕴,到底是詐尸還是另有隱情,我是刑警寧澤芝发,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布绪商,位于F島的核電站,受9級(jí)特大地震影響辅鲸,放射性物質(zhì)發(fā)生泄漏格郁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一独悴、第九天 我趴在偏房一處隱蔽的房頂上張望例书。 院中可真熱鬧,春花似錦刻炒、人聲如沸决采。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)树瞭。三九已至,卻和暖如春爱谁,著一層夾襖步出監(jiān)牢的瞬間晒喷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工访敌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凉敲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓捐顷,卻偏偏與公主長(zhǎng)得像荡陷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迅涮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容