首页 > 系统相关 >内存泄漏的几种情况

内存泄漏的几种情况

时间:2024-04-02 16:26:42浏览次数:22  
标签:泄漏 对象 托管 回收 几种 GC 内存 public

1、.Net 应用中的内存

1.1、托管堆

由 .NET 运行时(CLR)自动管理的内存区域,用于存储对象实例和数组等引用类型数据

在堆上分配的内存会通过垃圾回收器(GC)进行自动回收,对象的创建和销毁都是由GC负责管理。

1.2、非托管堆

不由CLR控制和管理,通常用于与非托管代码(如C、C++)进行交互、进行底层的系统编程或使用特定的外部库

非托管堆用于存储非托管代码中分配的对象,非托管代码通过内存分配函数(如 malloc)分配和管理非托管堆。

没有自动垃圾回收的机制,需要显示的释放资源(Dispose())。

最常用的非托管资源类型是包装操作系统资源的对象,如文件、窗口、网络连接、数据库连接、画刷、图像、图标等。

1.3、栈内存

栈内存的作用域仅限于所属的代码块或方法,用于存储函数的执行上下文,包括函数的参数、局部变量和函数返回地址等。

存储值类型数据和引用类型数据的引用。

栈内存的分配和释放是由编译器自动完成的,遵循先进后出的原则,具有较高的效率。

1.4、静态/常量存储区

存储Static变量(值类型或者引用类型的指针)及常量存储的区域。

 

2、内存泄漏

内存溢出(Out of Memory):当程序占用的内存超过了系统分配的最大内存时,会发生内存溢出错误。这通常发生在处理大数据集或无限递归时。

内存泄漏(Memory Leak):内存泄漏指的是程序在申请内存后,未能在不需要时正确释放,一直占用。严重会导致内存溢出。

在C#中,内存泄漏通常指的是由于长时间运行的应用程序或者某个操作导致的不再需要的对象无法被垃圾回收器回收的情况。这通常发生在以下几种情况:

2.1、事件订阅

原因:未取消的事件订阅可能导致订阅者对象始终保持对其的引用,从而无法释放。

解决方法:使用弱事件模式或者手动管理事件订阅,以确保订阅者不会阻止垃圾回收器回收其资源。

如下,弱未取消订阅,当EventPublisher的寿命超过EventSubscriber,那么你就已经造成了内存泄漏。 EventPublisher会引用EventSubscriber的任何实例,并且垃圾回收器永远不会回收它们。

public class EventPublisher
{
    public event EventHandler SomeEvent;

    public void PublishEvent()
    {
        // 发布事件
        SomeEvent?.Invoke(this, EventArgs.Empty);
    }

    public void UnsubscribeEvent(EventHandler handler)
    {
        // 解注册事件处理程序
        SomeEvent -= handler;
    }
}

public class EventSubscriber
{
    private EventPublisher publisher;

    public EventSubscriber(EventPublisher publisher)
    {
        this.publisher = publisher;
        // 订阅事件
        publisher.SomeEvent += HandleEvent;
    }

    private void HandleEvent(object sender, EventArgs e)
    {
        // 处理事件
    }

    public void UnsubscribeFromEvent()
    {
        // 解注册事件处理程序
        publisher.UnsubscribeEvent(HandleEvent);
    }
    
    ~EventSubscriber()
    {
        Console.WriteLine("实例{0}被回收", Id); //在GC时会触发
    }
}

internal class Task01
{
    public static void Run()
    {
        PublishEvent mychange = new PublishEvent();

        for (int i = 0; i < 100; i++)
        {
            EventSubscriber task = new Subscriber(mychange);
            //task.UnsubscribeFromEvent(); //如果忘记取消订阅,则会导致对象引用泄漏
        }
        //手动GC,如果没有取消订阅,终结器~Subscriber不会触发,取消了订阅后,~Subscriber才会触发
        GC.Collect();
        Console.ReadLine();
    }
}

 

2.2、静态变量、单例

原因:静态变量会在应用程序的整个生命周期内持有对象的引用,不会被垃圾回收,如果不慎使用,可能导致泄漏。

解决方法:避免使用全局静态变量或者确保在不需要时将它们设为null。

GC遍历所有GC Root对象并将其标记为“不可收集”。 然后,GC转到它们引用的所有对象,并将它们也标记为“不可收集”。 最后,GC收集剩下的所有内容。

那么什么会被认为是一个GC Root?

  1. 正在运行的线程的实时堆栈。
  2. 静态变量。
  3. 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)。

如下,任何MyClass的实例将永远留在内存中,从而导致内存泄漏。

public class MyClass
{
    static List<MyClass> _instances = new List<MyClass>();
    public MyClass()
    {
        _instances.Add(this);
    }
}

 

2.3、永不终止的线程

原因:实时堆栈会被视为GC root。 实时堆栈包括正在运行的线程中的所有局部变量和调用堆栈的成员。

解决方法:线程使用完后及时清理。

以下,timer一直将阻止GC回收。

    public class Scheduler
    {
        public Scheduler()
        {
            Timer timer = new Timer(Handle);
            timer.Change(0, 5000); //创建了一个timer,这个timer每隔5秒执行
        }

        private void Handle(object e)
        {
            Console.WriteLine("任务调度中……");
        }


        ~Scheduler()
        {
            Console.WriteLine("资源被释放");
        }
    }

    internal class Task01
    {
        public static void Run()
        {

            for (int i = 0; i < 3; i++)
            {
                Scheduler scheduler = new Scheduler();
            }
            GC.Collect();//手动GC
            Console.ReadLine();
        }
    }

 

2.4、非托管资源

问题:如文件句柄、数据库连接等,不受GC管理,如果没有正确释放,可能导致泄漏。

解决方法:实现IDisposable接口,在Dispose方法中释放非托管资源。使用using语句或try-finally块来确保资源被释放。

using (var instance = new MyClass())
{
    // ... 
}
MyClass instance = new MyClass();;
try
{
    // ...
}
finally
{
    if (instance != null)
        ((IDisposable)instance).Dispose();
}

在一个包含非托管资源的类中,关于资源释放的标准做法是:

1)继承IDisposable接口;
2)实现Dispose()方法,在其中释放托管资源和非托管资源,并将对象本身从垃圾回收器中移除(垃圾回收器不在回收此资源);
3)实现类析构函数,在其中释放非托管资源。
只要按照上面要求的步骤编写代码,该类就属于资源安全的类。

析构函数只能由垃圾回收器调用,Despose()方法只能由类的使用者用。

如下,在使用时,显示调用Dispose()方法,可以及时的释放资源,同时通过移除Finalize()方法的执行,提高了性能;如果没有显示调用Dispose()方法,垃圾回收器也可以通过析构函数来释放非托管资源,垃圾回收器本身就具有回收托管资源的功能,从而保证资源的正常释放,只不过由垃圾回收器回收会导致非托管资源的未及时释放的浪费。

public class BaseResource : IDisposable
{
    // 指向外部非托管资源
    private IntPtr handle;
    // 此类使用的其它托管资源.
    private Component Components;
    // 跟踪是否调用.Dispose方法,标识位,控制垃圾收集器的行为
    private bool disposed = false;
    // 构造函数
    public BaseResource()
    {
        // Insert appropriate constructor code here.
    }
    // 实现接口IDisposable.
    // 不能声明为虚方法virtual.
    // 子类不能重写这个方法.
    public void Dispose()
    {
        Dispose(true);
        // 离开终结队列Finalization queue,阻止GC调用Finalize方法
        GC.SuppressFinalize(this);
    }
    
    // 如果disposing 等于 true, 方法已经被调用
    // 或者间接被用户代码调用. 托管和非托管的代码都能被释放
    // 如果disposing 等于false, 方法已经被终结器 finalizer 从内部调用过,
    // 你就不能在引用其他对象,只有非托管资源可以被释放。
    protected virtual void Dispose(bool disposing)
    {
        // 检查Dispose 是否被调用过.
        if (!this.disposed)
        {
            // 如果等于true, 释放所有托管和非托管资源
            if (disposing)
            {
                // 释放托管资源.
                Components.Dispose();
            }
            // 释放非托管资源
            CloseHandle(handle);
            handle = IntPtr.Zero;
            // 注意这里是非线程安全的.
            // 在托管资源释放以后可以启动其它线程销毁对象,
            // 但是在disposed标记设置为true前
            // 如果线程安全是必须的,客户端必须实现。
        }
        disposed = true;
    }
    
    // 使用interop 调用方法,清除非托管资源.
    [System.Runtime.InteropServices.DllImport("Kernel32")]
    private extern static Boolean CloseHandle(IntPtr handle);
    
    // 使用析构函数来实现终结器代码,由GC调用
    // 这个只在Dispose方法没被调用的前提下,才能调用执行。
    ~BaseResource()
    {
        // 不要重复创建清理的代码.
        // 基于可靠性和可维护性考虑,调用Dispose(false) 是最佳的方式
        Dispose(false);
    }
    
    // 允许你多次调用Dispose方法,
    // check to see if it has been disposed.
    public void DoSomething()
    {
        if (this.disposed)
        {
            thrownew ObjectDisposedException();
        }
    }
    
    public static void Main()
    {
        // Insert code here to create
        // and use a BaseResource object.
    }
}
.NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。当你用Dispose方法释放未托管对象的时候,应该调用GC.SuppressFinalize。GC.SuppressFinalize会阻止GC调用Finalize方法。因为Finalize方法的调用会牺牲部分性能。如果你的Dispose方法已经对委托管资源作了清理,就没必要让GC再调用对象的Finalize方法。 注意: 在.NET中应该尽可能的少用析构函数释放资源。在没有析构函数的对象在垃圾处理器一次处理中从内存删除,但有析构函数的对象,需要两次,第一次调用析构函数,第二次删除对象。而且在析构函数中包含大量的释放资源代码,会降低垃圾回收器的工作效率,影响性能。所以对于包含非托管资源的对象,最好及时的调用Dispose()方法来回收资源,而不是依赖垃圾回收器。

 

2.5、LOH泄漏

.NET CLR中对于大于85000字节的内存既不像引用类型那样分配到普通堆上,也不像值类型那样分配到栈上,而是分配到了一个特殊的称为LOH的内部堆上,这部分的内存只有在GC执行完全回收,也就是回收二代内存的时候才会回收。因此,考虑如下情形:

假设你的程序每次都要分配一个大型对象(大于85000字节),但却很少分配小对象,导致2代垃圾回收从不执行,即使这些大对象不再被引用,依然得不到释放,最终导致内存泄漏。

解决方法:由于LOH本身的特性,在程序中,我们当尽量避免频繁的使用大内存对象,如果不能就应当尽量避免内存碎片。

如下实例中我们交替产生了150000字节和85000字节的大内存对象,同时我们模拟了GC的频繁触发,我们通过Winddbg中的!dumpheap命令分析,就会看到内存中出现了大量的碎片,Free和Live交替出现。但如果我们把数据的大小固定住85000,那么后续新分配的对象就有很大概率继续使用前面的空闲空间,大大减少了内存碎片。
    internal class Task01
    {
        public static void Run()
        {
            List<byte[]> objs = new List<byte[]>();
            for (int i = 0; i < 500; i++)
            {
                //两种大对象交替出现
                if (i % 2 == 0)
                {
                    objs.Add(new byte[150000]);
                    objs[i] = null;
                    if (i % 10 == 0)
                        GC.Collect(); //模拟GC触发
                }
                else
                {
                    objs.Add(new byte[85000]);
                }
            }
            Console.WriteLine("执行完毕");
            Console.ReadLine();
        }
    }

在应用中,我们对于大对象的使用通常可能来自于某些大对象的更新缓存,比如:

        public static void Main()
        {
            Console.WriteLine("开始执行");
            byte[] bigFastCache = new byte[150000];
            for (int i = 0; i < 500; i++)
            {
                //更新操作,数据大小会不同
                if (i % 2 == 0)
                {
                    bigFastCache = new byte[150000];
                }
                else
                {
                    bigFastCache = new byte[85000];
                }
            }
			GC.Collect(); //模拟GC触发
            Console.WriteLine("执行完毕");
            Console.ReadLine();
        }

只是对于这个数据的交替更新,其对象创建和销毁的开销都很大,这里我建议使用池化对象,在使用的时候从池中租借一个新对象,使用完成后归还即可:

        public static void Main()
        {
            Console.WriteLine("开始执行");

            byte[] bigFastCache = null;
            var bigPool = ArrayPool<byte>.Shared; //使用池化对象要慎重
            for (int i = 0; i < 500; i++)
            {
                //更新操作,数据大小会不同
                if (i % 2 == 0)
                {
                    bigFastCache = bigPool.Rent(100000);
                    Console.WriteLine(bigFastCache.Length);
                }
                else
                {
                    bigFastCache = bigPool.Rent(85000);
                    Console.WriteLine(bigFastCache.Length);
                }
                bigPool.Return(bigFastCache);
            }
            Console.WriteLine("执行完毕");
            Console.ReadLine();
        }

如果你用到了Dictionary大对象缓存,建议提前在构造函数中设置Capacity来优化GC,这样对的性能和内存占用都有好处。

 

3、内存优化

托管堆内存优化:

  • 使用对象池:避免频繁地创建和销毁对象,可以使用对象池来重复利用对象实例。
  • 减少装箱和拆箱:尽量使用泛型集合(如`List`)来避免值类型的装箱和拆箱操作。
  • 及时释放资源:手动释放不再使用的托管内存,如调用对象的`Dispose()`方法或使用`using`语句来确保及时释放资源。

非托管堆内存优化:

  • 尽量避免直接使用非托管内存:推荐优先使用托管内存,仅在必要时与非托管代码交互,并使用`Marshal`类的相关方法来管理非托管内存的分配和释放。
  • 避免内存泄漏:确保将非托管内存正确释放,避免内存泄漏问题。

栈内存优化:

  • 尽量使用局部变量:将数据存储在栈上的局部变量中,而不是使用类的实例变量。这样可以减少托管堆内存的压力,同时也提高访问速度。
  • 使用值类型:对于小型数据,考虑使用值类型而不是引用类型来减少内存开销和垃圾回收的成本。

其他优化技巧:

  • 避免使用过多的字符串拼接操作:频繁的字符串拼接可能会导致内存碎片和性能下降,尽量使用`StringBuilder`类来处理大量字符串拼接。
  • 缓存重复计算结果:如果有一些计算结果会被重复使用,可以将结果缓存起来,避免重复计算和内存消耗。
  • 使用合适的数据结构:选择适当的数据结构和算法来优化内存和性能,如使用哈希表、集合等数据结构。
  • 使用性能分析工具:使用性能分析工具(如.NET Memory Profiler)来检测内存泄漏、高内存使用和潜在性能问题。

需要注意的是,对内存的管理和操作大部分都是由 .NET 运行时处理的。开发者无需过多关注内存管理的细节,因为托管堆内存的垃圾回收机制可以自动处理对象的分配和释放。然而,在特定情况下,如与非托管代码交互、进行性能优化或处理大量数据等,了解这些内存区域的概念和用法可以帮助编写更高效和可靠的代码。

 

标签:泄漏,对象,托管,回收,几种,GC,内存,public
From: https://www.cnblogs.com/xixi-in-summer/p/18104369

相关文章

  • 程序员常用的几种算法
    1.排序算法:•冒泡排序(BubbleSort)•选择排序(SelectionSort)•插入排序(InsertionSort)•快速排序(QuickSort)•归并排序(MergeSort)•堆排序(HeapSort)•计数排序(CountingSort)、桶排序(BucketSort)等2.查找算法:•线性搜索(LinearSearch)•......
  • JUC:java内存模型(如何保证?可见性、原子性、有序性)
    文章目录java内存模型可见性解决方法原子性有序性流水线技术模式之Balking(犹豫)java内存模型JMM即JavaMemoryModel,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。JMM体现在以下几个方面:原子性-保证指令不......
  • 内存,寄存器,缓存,cache
    对上面这几个名词认识,但要说对他们的理解,不知道。在项目开发过程中,经常会遇到说什么从缓存读取数据,拷贝到内存,什么值存在寄存器中等等,但都是傻傻分不清。没有自己的理解。下面从volatile关键字引入本节的学习原文链接:https://blog.csdn.net/Goforyouqp/article/details/1313099......
  • java几种代理模式的实现方式
    1.代理模式代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实......
  • 执行计划不走索引的几种情况总结
    优化器不想用索引,主要原因是优化器认为走索引还不如走顺序扫描代价低,因为索引扫描对应的是离散IO,我们可以通过调整random_page_cost告诉优化器随机IO代价值,非特殊情况不建议修改此值。1.表太小场景经常有开发问,为什么有索引而不走索引呢?因为优化器认为走索引方式太慢了!test=#c......
  • 【吊打面试官系列】Redis篇 -都有哪些办法可以降低 Redis 的内存使用情况呢?
    大家好,我是锋哥。今天分享关于【都有哪些办法可以降低Redis的内存使用情况呢?】面试题,希望对大家有帮助;都有哪些办法可以降低Redis的内存使用情况呢?如果你使用的是32位的Redis实例,可以好好利用Hash,list,sortedset,set等集合类型数据,因为通常情况下很多小的Key......
  • 动态内存管理【malloc,calloc,realloc和free的理解】【柔性数组的概念】
    一.为什么要有动态内存分配我们知道,当我们创建变量的时候,我们会向系统申请一定大小的空间内存。比如inta=10或者intarr[10];我就向内存申请了4或者40个字节的大小来存放数据。但是当我们一旦申请好这个空间,大小就无法调整了。但是对于空间的需求,不仅仅就只有上面的情况。有时......
  • <汇编语言> 3. 寄存器(内存) | 检测点 3.2
    (1)补全下面的程序,使其可以将10000H1000FH中的8个字,逆序复制到20000H2000FH中。逆序复制的含义如图3.17(P70)所示(图中内存里的数据均为假设)。movax,1000Hmovdx,ax//栈段为1000:00H~1000:0FH_pushax,1000H___PUSHss,ax_____//栈顶指针为0FH+1=10H_pushsp,00......
  • Android 10.0 lowmemorykiller低内存时,禁止某个app被kill掉功能实现
    1.前言在10.0的系统定制化开发中,在对于系统lowmemorykiller低内存的时候,应用保活功能是非常重要的,就是在低内存的情况下禁止某个app被杀掉,所以就需要从lowmemorykiller机制入手,在杀进程的相关流程中进行分析来实现进程避免被杀掉,接下来就来实现这个功能2.lowmemorykiller低......
  • Python 爬虫html内存 re.findall 正则提取span
    前言全局说明爬虫html内存re.findall正则提取一、百度首页热搜(和百度原网页代码有修改)需求:提取内容文字。<ulclass="s-hotsearch-content"id="hotsearch-content-wrapper"><liclass="hotsearch-itemodd"data-index="0"><spanclass=&q......