.NET 垃圾回收与内存泄漏

原文:.NET 垃圾回收与内存泄漏

> 前言
相信大家一定听过,看过甚至遇到过内存泄漏。在 .NET 平台也一定知道有垃圾回收器,它可以让开发人员不必担心内存的释放问题,因为它会自定管理内存。但是在 .NET 平台下进行编程,绝对不会发生内存泄漏的问题吗?答案是否定的,就算有了自动内存管理的垃圾回收器,也会发生内存泄漏。本文就讨论下 .NET 平台的垃圾回收器是如何工作的,进而当我们在编写 .NET 程序时避免发生内存泄漏的问题。

> 垃圾回收的基本概念
“垃圾”指的是事先分配过但后来不再被使用的内存。
垃圾回收背后的一个基本观念是:“无限访问的内存”,但是从来没有无限的内存,当机器需要分配内存但不够的时候,就需要把之前不再使用的内存——“垃圾”回收再利用。
.NET 的垃圾回收器正是这样做的:
.NET Framework 的垃圾回收器管理应用程序的内存分配和释放。每当您创建新对象时,公共语言运行时都会从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行时就会继续为新对象分配空间。 但是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。(引用 MSDN 垃圾回收

> 垃圾回收器的工作场景
每当我们创建一个对象的时候,系统会为新对象分配一块内存,如果有足够的可用内存则会直接分配;但是当内存不足的时候,此时垃圾回收器会进行一次回收操作,把不再使用的对象释放,转化为可用的内存供新对象使用。

看似很简单的工作步骤,但是垃圾回收器怎么知道确保不再使用的对象的呢?

> 垃圾回收算法
当进行一次垃圾回收操作时,会分三个步骤进行:
1. 先假设所有对象都是垃圾;
2. 标记出正在使用的对象;
  标记依据:
  a. 被变量引用的对象,仍然在作用域中。
    比如某个类中的某个方法,方法执行了一半,如果此时发生垃圾回收,那么方法块中的变量都在作用域中,那么它们都会被标记为正在使用。
  b. 被另一个对象引用的对象,仍在使用中。
3. 压缩:释放第二步中未标记的对象(不再使用,即“垃圾”)并将使用中的对象转移到连续的内存块中。
  只要垃圾回收器释放了能释放的对象,它就会压缩剩余的对象,把它们都移回堆的端部,再一次形成一个连续的块。

备注:
垃圾回收器为了提升性能,使用了代机制,新建的对象是新一代,较早创建的对象是老一代,最近创建的对象是第0代。为了描述垃圾回收器的基本原理,本文不深入讨论代机制。

总之,有了垃圾回收器,我们不必自己实现代码来管理应用程序所用的对象的生存期。

既然有了自动内存管理功能的垃圾回收器,为什么还会发生内存泄漏呢?

> 托管与非托管
由公共语言运行库环境(而不是直接由操作系统)执行的代码称作托管代码,运行在 .NET 框架下,受 .NET 框架管理的应用或组件称作托管资源。.NET 中超过80%的资源都是托管资源,如 int, string, float, DateTime。
非托管资源是 .NET 框架之外的,最常见的一类非托管资源就是包装操作系统资源的对象,例如文件,窗口或网络连接,对于这类资源虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。所以,对于非托管资源,在应用程序中使用完之后,必须显示的释放它们。
所以,大部分内存泄漏都是非托管资源内存泄漏:没有显示的释放它们。

> 非托管资源内存泄漏
一个会导致内存泄漏的类:

public class Foo
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}

调用 Foo 类:

static void Main(string[] args)
{
    Foo foo = new Foo();
    foo = null;

    Thread.Sleep(int.MaxValue);
}

foo 虽然设置为 null,但是 foo 中的字段 _timer 依然存活,Elapsed 事件继续执行:

此类中,_timer 对象就是非托管对象,由于 _timer 的 Elapsed 事件,.NET Framework 会保持 _timer 永远存活,进而 _timer 对象会保持 Foo 实例永远存活,直到程序关闭。

为了解决这个问题,我们要显示的释放 _timer 对象:Foo 类继承 IDisposable 接口,修改后的类:

public class Foo : IDisposable
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
        _timer.Dispose();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}

 

再次调用 Foo 类,并显示调用 Dispose 方法: 

static void Main(string[] args)
{
    Foo foo = new Foo();
    foo.Dispose();
    foo = null;

    Thread.Sleep(int.MaxValue);
}

foo 设置为 null,_timer 对象也同时被回收,Elapsed 事件停止:

> 非托管资源的垃圾回收
1. 析构函数。
2. 实现 IDisposable 接口。
  在我们编写代码时,一个简单的方法就是查看类中定义的字段是否有继承 IDisposable 接口的,如果有,那么当前的类也应继承 IDisposable 接口。在使用完非托管资源时,要及时调用 Dispose 方法释放资源:

Label label = new Label();
this.Controls.Add(label);
this.Controls.Remove(label);
label.Dispose();

更好的方式是使用 using,using 会在编译代码的时候自动创建 try/finally 语句块,在 finally 语句块中自动调用 Dispose 方法。

using (Label label = new Label())
{
    this.Controls.Add(label);
    this.Controls.Remove(label);
}

 

> 避免内存泄漏的几点建议
除了刚刚提到的非托管资源,还有几点需要注意:
1. 订阅事件,不再使用时要记得取消订阅。
2. 不要大量使用静态字段,静态字段会永远存活,一个静态的集合很容易引起内存溢出。

时间: 2024-09-21 09:24:47

.NET 垃圾回收与内存泄漏的相关文章

.NET深入学习笔记(3):垃圾回收与内存管理

今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会问相关的问题.那么你是否也遇到这样的问题呢?比如你清楚.Net的垃圾回收机制吗?你能简述一下GC的工作原理吗?怎么样才能有效的管理内存呢?Using语句体内实例化的对象有什么作用?等等相关问题.下面我们就来详细讨论一下.相信你看完以后也可以面试别人. 本节的组织如下,1..Net的类型和内存分配2.GC垃圾收

简析Android的垃圾回收与内存泄露

Android系统是运行在Java虚拟机上的,作为嵌入式设备,内存往往非常有限,了解Android的垃圾回收机制,可以有效的防止内存泄露问题或者OOM问题.本文作为入门文章,将浅显的讨论垃圾回收与内存泄露的原理,不讨论Dalvik虚拟机底层机制或者native层面的问题. 1. 基础 在分析垃圾回收前,我们要复习Java与离散数学的基础. 实例化:对象是类的一个实例,创建对象的过程也叫类的实例化.对象是以类为模板来创建的.比如Car car = new Car();,我们就创造了一个Car的实例

JS闭包、作用域链、垃圾回收、内存泄露相关知识小结_javascript技巧

补充: 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收 闭包的定义及其优缺点 闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量 闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露. 闭包是javascript

jvm 内存与垃圾回收

你对JVM内存组成结构和JVM垃圾回收机制是否熟悉,这里和大家简单分享一下,希望对你的学习有所帮助,首先来看一下JVM内存结构,它是由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示. JVM学习笔记 JVM内存管理和JVM垃圾回收 JVM内存组成结构 JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,

浅析Node.js中的内存泄漏问题

  这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题

浅析Node.js中的内存泄漏问题_node.js

 这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题文

javascript垃圾收集机制与内存泄漏详细解析_javascript技巧

javascript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中的使用的内存.而在C和C++之类的语言中,开发人员的一项基本任务就是手动跟踪内存的使用情况,这是造成许多问题的一个根源.在编写javascript程序时候,开发人员不用再关心内存使用的问题,所需内存的分配 以及无用的回收完全实现了自动管理.这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其中占用的内存.为此,垃圾收集器会按照固定的时间间隔(或代码执行中预设的收集时间),周期性的执行这一操作.

Android性能优化之利用强大的LeakCanary检测内存泄漏及解决办法_Android

前言: 最近公司C轮融资成功了,移动团队准备扩大一下,需要招聘Android开发工程师,陆陆续续面试了几位Android应聘者,面试过程中聊到性能优化中如何避免内存泄漏问题时,很少有人全面的回答上来.所以决定抽空学习总结一下这方面的知识,以及分享一下我们是如何检测内存泄漏的.我们公司使用开源框架LeakCanary来检测内存泄漏. 什么是内存泄漏? 有些对象只有有限的生命周期.当它们的任务完成之后,它们将被垃圾回收.如果在对象的生命周期本该结束的时候,这个对象还被一系列的引用,这就会导致内存泄漏

JVM调优总结(九)-新一代的垃圾回收算法

垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解 决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受 的.这类应用可能要求请求的返回时间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个 相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的. 分代垃圾回收方式确实也考虑了实时性