[转]有了垃圾回收,还会不会发生内存泄漏?

问题发现##

这个问题是我在写C++时考虑到的,C++需要手动管理内存,虽然现在标准库中提供了一些智能指针,可以实现基于引用计数的自动内存管理,但现实环境是很复杂的,我们仍要注意循环引用的问题。还有一个容易被忽视的问题就是对象间关系的“占有”和“非占有”,这个问题其实在具有GC的C#和Java中也一样存在。

目前.NET和Java的GC策略都属于Tracing garbage collection,基本原理是从一系列的root开始,沿着引用链进行遍历,对遍历过的对象进行标记(mark),表示其“可达(reachable)”,然后回收那些没有标记的,即“不可达”对象所占用的内存。如果你的代码中明明有的对象已经没用了,但在某些地方仍然保持有对它的引用,就会造成这个对象长期处于“可达”状态,以至其占用的内存无法被及时回收。

对象关系的问题##

占有与非占有###

好吧,这两个词是我自己发明的。这两个词是针对“拥有”而言的,占有 是表示强的拥有,宿主对象会影响被拥有对象的生命周期,宿主对象不死,被拥有的对象就不会死;非占有 表示弱的拥有,宿主对象不影响被拥有对象的生命周期。

在处理对象间关系时,如果应该是非占有关系,但却实现成了占有关系,则占有关系就会妨碍GC对被占有对象的回收,轻则造成内存回收的不及时,重则造成内存无法被回收。这里我用C#实现观察者模式作为示例:
<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("垃圾回收结束");

    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;
}

}
</pre>

<strong>这段代码有什么问题吗?</strong>在AttachSubscribers方法里,创建了两个订阅者,并进行了订阅,这里的两个订阅者都是在局部创建的,也并没有打算在外部引用它们,它们应该在不久的某个时刻被回收了,但是由于同时它们又存在于发布者的订阅者列表里,发布者“占有”了订阅者,虽然它们都没用了,但暂时不会被销毁,如果发布者一直活着,则这些没用的订阅者也一直得不到回收,那为什么不调用UnSubscribe呢?因为在实际中情况可能很复杂,有些时候UnSubscribe调用的时机会很难确定,而且发布者的任务在于登记和通知订阅者,不应该因此而“占有”它们,不应干涉它们的死活,所以对于这种情况,可以使用“弱引用”实现“非占用”。

弱引用###

弱引用是一种包装类型,用于间接访问被包装的对象,而又不会产生对此对象的实际引用。所以就不会妨碍被包装的对象的回收。

给上面的例子加入弱引用:
<pre>
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;
        }
    }
}

}
</pre>

其实弱引用也不是完美的解决方案,因为限制了API使用者的自由,当然这里也没打算实现一个通用的、完美的解决办法,只是想通过个例子让你知道,即使是在有GC的情况下,不注意代码设计的话,仍有可能会发生内存泄漏的问题。

非托管资源##

GC不会释放非托管资源吗?###

GC的作用在于清理托管对象,托管对象是可以定义析构方法(准确点说应该叫finalizer,C#中的~类名,Java中的finalize)的,这个方法会在托管对象被GC回收前被调用,析构方法里完全可以通过调用平台API释放非托管资源(实际上很多托管对象的实现也都这么做了),也就是说GC是可以释放非托管资源的。以下代码摘自.NET类库中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的析构方法里调用了Dispose,继而调用了_handle.Dispose,_handle.Dispose内部调用的可能是一些native api(一般是用C实现的)。

但是如果托管对象的生命很长,甚至比如说它的静态的,则它内部包装的资源将一直得不到回收,而且托管对象内部包装资源可能属于“紧张的资源”,比如非托管内存、文件句柄、socket连接,这些资源是必须要被及时回收的,比如文件句柄不及时释放会导致该文件一直被占用,影响其它进程对该文件的读写、socket连接不及时释放会导致端口号一直被占用,为了解决这些问题,我们需要显式地去释放这些资源。

Dispose模式###

一个常见的做法就是在对象中定义一个方法来专门释放这些非托管资源,比如叫close, dispose, free, release之类,然后在不需要使用此对象时显式调用这个方法。C#中的IDisposable接口和Java中的Closeable接口就是这个作用,因为大多数带GC的语言都使用这种设计,所以这也算是一种模式。

伪代码示例:
<pre>
File f = File.openWrite("data.txt");
f.writeBytes((new String("Hello, world!")).getBytes("ascii"));
f.close();
</pre>

这样就够了吗?如果close前发生异常或直接return了怎么办? — finally语句块

finally语句块保证了其中的语句一定会被执行,配合close方法,就能确保非托管资源的释放。

C++中没有finally语句结构,这并不奇怪,因为C++有RAII机制,对象的销毁是确定的,而且确保析构函数的调用,所以不需要finally这种语法。

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容