zoukankan      html  css  js  c++  java
  • 自动内存管理(垃圾回收)

    1、理解垃圾回收平台的基本工作原理

      在面向对象的环境中,每个类型都代表可供程序使用的一种资源,使用资源,需要分配内存。如何访问资源?

    •   调用IL指令newobj,为代表资源的类型分配内存。在C#中使用new操作符,编译器就会自动生成该指令。

        CLR执行了以下操作(托管堆分配资源):

        1、计算类型(及其所有基类型)的字段需要的字节数

        2、加上对象的开销所需要的字节数(类型对象指针,同步块索引)两个字段占用的空间都是一样的,如果是32位的,则32位,即8字节。64的,16字节。

        3、CLR检查保留区域是否能够提供分配对象所需的字节数,如有必要就提交存储(将存储空间“交”给预订者)。如果托管堆有足够的可用空间,对象会被放       入。注意对象是在NextObjPtr指针(托管堆维护的一个指针,指向下一个对象在队中的分配位置),并且为它分配的字节会被清零。接着,调用类型的实例构造器(为this参数产地NextObjPtr),IL指令newobj将返回对象的地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样会得到一个新值,它指向下一个对象放入托管堆时的地址。

    •   初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器扶着设置该初始状态。
    •   访问类型的成员来使用资源
    •   摧毁资源的状态以进行清理。(如:Finalize,Dispose,Close)
    •   释放内存,垃圾回收器独自负责这一步

      而C#中有一个机制为开发人员简化对内存管理任务——垃圾回收

      应用程序调用new操作符创建对象时,可能没有足够的地址空间来分配该对象。托管堆将对象需要的字节数加到NextObjPrt指针中的地址上来检测这种情况。如果结果值超过了地址空间的末尾,表示托管堆已满,必须执行一次垃圾回收。

    写个程序看看效果

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Timer t = new Timer(TimerCallback, null, 0, 2000);
    
                Console.Read();
            }
    
            // 回调方法
            private static void TimerCallback(object o)
            {
                //回调时候,显示日期
                Console.WriteLine("In TimerCallback :"+DateTime.Now.ToString());
                // 垃圾回收
                GC.Collect();
    
            }
        }
    }

    在上述代码中,TimerCallback方法只被调用了一次!!

    分析:

    因为回调函数TimerCallback中,通过调用GC.Collect()强制垃圾回收一次。回收开始时,垃圾回收器首先假定堆中所有对象都是不可达的(垃圾);eg:Timer对象。然后,垃圾回收器检查应用程序的根,发现在初始化后,Main方法没有再调用变量t.对象不可达,回收对象t的内存,所以TimerCallback只被调用了一次。

    而在调试模式下,“监视”窗口查看t引用对象,因为对象已经被回收,所以调试器无法显示该对象。

    当然微软给了一个解决方案。

    JIT编译器将方法的IL代码编译成本地代码时,JIT编译器会检查两点:定义方法的程序集在编译时有没有优化;进程当前在一个调试器中执行。如果这两点都城里,JIT编译器在生成方法的内部根表时,会将所有变量的生存期手动延长至方法结束。换言之,JIT编译器自己骗自己,让自己认为Main中的t变量必须生存到方法结束。所以,如果发生了垃圾回收,垃圾回收器会认为t仍然是一个根,t引用的Timer对象仍然可达。Timer对象会在回收中存活,TimerCallback方法会被重复调用,直至Console.Read()方法返回而且Main()方法退出。

    同理,命令行中重新编译程序,但是制定C#编译器的/debug+编译器开关。运行执行文件,仍会发现TimerCallback多次调用。这是因为当JIT编译方法时,JIT编译器会检查定义方法的程序集是否应用了System.Diagnostics.DebuggableAttribute,而且它的DebugingModes的DisableOptimizations标志是否被设置。只要JIT编译器发现该编制已设置,同样会编译方法时将所有变量的生存期手动延长到方法结束。指定/debug+编译器开关后,C#编译器会在最终的程序集中自动生成该attribute和标志。注意,C#编译器的/optimize+编译器开关会将DisableOptimizations禁止的优化重新恢复,所以做这个测试的时候,不要打开这个开关。

      static void Main(string[] args)
            {
                Timer t = new Timer(TimerCallback, null, 0, 2000);
    
                Console.Read();
    
                t = null;
            }

    但是,如果编译器(不使用/debug+编译器开关),TimerCallback仍只调用了一次。这里的问题在于,JIT编译器是一个优化编译器,将局部变量或者参数变量设为null,等价于根本不引用该变量。相当于,JIT编译器,会将 t=null;这段代码优化掉;

      static void Main(string[] args)
            {
                Timer t = new Timer(TimerCallback, null, 0, 2000);
    
                Console.Read();
    
                t.Dispose();
            }

    这样TimerCallback正确的重复调用,现在只有t存活,才会调用Dispose()方法,变量t可到达。

    我想有时候项目里的用Debug编译出了问题,就在这里???

     如果我们在同一个方法中,创造过多的引用对象,这样会对程序的性能造成影响,因此我们要遵守以下的规则,减少GC的工作量

    1、将常用的局部变量提升为成员变量

    所有的引用类型,包括那些局部变量,都会分配到堆上。在函数退出后,函数内的所有局部变量都会立即变成垃圾对象。所以我们可以得出结论:

    若是某个引用类型(值类型无所谓)的局部变量用于被频繁调用的例程中,那么应该将其提升为成员变量。这既有助于减轻GC的负担,也可以提升程序运行的效率。

    代码如下

      protected override void OnPaint(PaintEventArgs e)
      {
         using (Font myFont = new Font("Arial", 10.0f))
         {
              e.Graphics.DrawString(DateTime.Now.ToString(), myFont, Brushes.Black, new Point(0, 0));
         }
         base.OnPaint(e);
       }

    将会被非常频繁的调用,每次调用都会创建一个Font对象,而包含的内容完全和上一次一样。所以GC需要每次都为你清扫这些垃圾,严重影响了应用程序的效率。其实我们完全可以将Font对象提升为成员变量,是每次窗体重绘时能够重用该Font对象:

     Font myFont = new Font("Arial", 10.0f)
    
     protected override void OnPaint(PaintEventArgs e)
      {
         e.Graphics.DrawString(DateTime.Now.ToString(), myFont, Brushes.Black, new Point(0, 0));
        
         base.OnPaint(e);
       }

    2、为常用的类型实例提供静态对象

    静态成员变量可以让引用类型在类的各个实例中共享。我们可以通过提供了一个类,存放某个类型常用的实例的单例对象,这样可以避免创建重复的对象。.NET Framework 的类库中就有很多这样的做法:Brush 类包含了一系列的静态Brush对象,每个都包含了一种常用的颜色。它们的简要实现如下:

        private static Brush blackBrush;
        public static Brush Black
        {
        get
           {
            if (blackBrush == null)
                blackBrush = new SolidBrush(Color.Black);
               return blackBrush;
            }
         }

    3.为不可变类型提供可变的创建对象

     System.String类型时一个不可变类型:即在构造一个字符串对象后,其内容不能被修改。如果对一个字符串进行修改时,实际上时创建了一个新的字符串对象,从前的字符串对象也就变成了垃圾。看下面的代码:

    string smg="how";
    smg+="do";
    smg+="you";
    smg+="do"

    string类型的+=操作符会创建一个新的字符串对象并返回,对于这类拼接字符串的工作应该交给更适合的string.Format()方法:

    string msg = string.Format("{0} {1} {2}{3}", "How", "do", "you", "do");

    如果需要进行一些比简单拼接字符串更加复杂的工作,可以考虑使用StringBuilder类,该类是一个可变的字符串,用来创建不可变的string对象。对于经常变化的stirng对象,使用StringBuilder对象来替换是一个非常好的选择 —— 当我们的某个设计需要不可变类型时,应该考虑提供一个创建对象,专门负责分步地构造出最终对象(例如:string对象之于StringBuilder对象)。这样既可让用户分步地创建出最终对象,也可以保证对象的不可变性。

     

     

     

  • 相关阅读:
    MySQL学习笔记
    Git常用命令
    MacBook Pro m1安装swoole PHP版本7.4
    斐波那契数列实现的2种方法
    归纳一些比较好用的函数
    阶乘的实现
    冒泡排序
    PHP上传图片
    PHPStorm常用快捷键
    DataTables的使用
  • 原文地址:https://www.cnblogs.com/lstcJaney/p/2793536.html
Copyright © 2011-2022 走看看