发生内存泄漏?

时间:2022-07-15 20:56:56

有了GC还会不会发生内存泄漏?

问题的发现

这个问题是我在写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<ISubscriber> _subscribers = new List<ISubscriber>();

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<ISubscriber> _subscribers = new List<ISubscriber>();

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<WeakReference<ISubscriber>> _subscribers = new List<WeakReference<ISubscriber>>();

public void Notify()
{
for (var i = 0; i <