[转]有了垃圾回收,还会不会发生内存泄漏?
问题发现##
这个问题是我在写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这种语法。