zoukankan      html  css  js  c++  java
  • 发生内存泄漏?

    有了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 < 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<ISubscriber>(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;
                }
            }
        }
    }

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

    非托管资源

    GC不能释放非托管资源吗?

    GC的作用在于清理托管对象,托管对象是可以定义析构方法(准确点说应该叫finalizer,C#中的~类名,Java中的finalize)的,这个方法会在托管对象被GC回收前被调用,析构方法里完全可以释放非托管资源(实际上很多托管对象的实现也都这么做了),也就是说GC是可以释放非托管资源的

    但是GC的运行时间是不确定的,现在计算机的内存也都足够大,内存迟点回收不会有什么问题,但托管对象内部包装的其它资源可能属于“紧张的资源”,比如非托管内存、文件句柄、socket连接,这些资源是必须要被及时回收的,比如文件句柄不及时释放会导致该文件一直被占用,影响其它进程对该文件的读写、socket连接不及时释放会导致端口号一直被占用,那如何保证释放的及时呢?

    Dispose模式

    方法很简单,就是在对象中用一个方法来专门释放这些非托管资源,比如叫closedisposefreerelease之类的,然后显式调用这些方法。C#中的IDisposable接口和Java中的Closeable接口就是这个作用,因为大多数带GC的语言都使用这种设计,所以这也算是一种模式。

    伪代码示例:

    File f = new File("data.txt");
    f.writeBytes((new String("Hello, world!")).getBytes("ascii"));
    f.close();

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

    finally语句块保证了其中的语句一定会被执行,配合close方法,就能确保非托管资源的及时释放。(注:不调用close其实一般来讲非托管资源也是会被释放的,只是这种释放不够“及时”,因为要等到托管对象被回收

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

    结语

    其实以上所列举的种种情况,大多数情况资源最终都会得到回收,只是回收不够及时,但这种回收不及时在资源紧张或出现极端情况时,还是有可能会发生内存泄漏的,所以说不是有了GC就可以高枕无忧了。

     


    作者:taney 
    出处:http://taney.cnblogs.com 

  • 相关阅读:
    springboot+websocket 归纳收集
    flask-admin 快速打造博客 系列一
    Flask-sqlalchemy 语法总结
    python 小问题收集
    pyenv+virtual 笔记
    flask_admin 笔记七 扩展功能
    Flask_admin 笔记六 modelView的内置方法
    spring boot 项目中 maven打第三方lib包时遇到的问题以及解决方法
    java 字符串压缩长度并解压
    highcharts中放aqi及6要素,再加上气象5要素的图
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/5474887.html
Copyright © 2011-2022 走看看