zoukankan      html  css  js  c++  java
  • 垃圾收集器原理

    引言

    在编程的过程中,你是否遇到过OutOfMemeryException的异常?程序在做性能测试时,应用服务器程序消耗的内存不断上升?在使用开源框架时,由于没有及时的Dispose而导致程序的异常发生?而造成这些异常的原因都是没有合理的释放内存导致的。我们不经要问,C#框架下不是有GC(自动垃圾收集器)吗?那么为什么还会出现如此异常错误呢?GC到底何时执行,执行时又做了什么?GC对性能的影响?怎样合理的释放资源呢?下面我们来揭开垃圾收集器的神秘面纱。

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

    1.1 基本原理分析

    我们知道,C#是CLR(Common Language Runtime公共语言运行库)下的一种托管代码语言,它的类型和对象在应用计算机内存时,大体用到两种内存,一种叫堆栈,另一种叫托管堆。C#中主要分为值类型和引用类型,当声明一个值类型对象时,会在栈中分配适当大小的内存,内存空间存储对象的值。其中维护一个栈指针,它包含栈中下一个可用内存空间的地址。当一个变量离开作用域时,栈指针向下移动并释放变量所占用的内存,所以它任然指向下一个可用地址;当声明一个引用类型对象时,引用变量也利用栈,但这时栈包含的只是对另一个内存位置的引用,而不是实际的值。这个位置是托管堆中的一个地址,和栈一样,它也维护一个指针,包含堆中下一个可用内存空间的地址。
    我们来写一个简单的事例代码,看看它内部到底发生了什么?

    namespace SourceDemo
    {
    	class Program
        { 
            static void Main(string[] args)
            {
                int iTotal = 1;
                Order order = new Order();
            }
        }
        class Order
        {
        }
    }
    

    通过ILDASM.EXE工具查看对应的IL代码如下:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint					
      // Code size 10 (0xa)
      .maxstack  1  
      .locals init ([0] int32 iTotal,
               [1] class SourceDemo.Order order)
      IL_0000:  nop
      IL_0001:  ldc.i4.1
      IL_0002:  stloc.0
      IL_0003:  newobj     instance void SourceDemo.Order::.ctor()
      IL_0008:  stloc.1
      IL_0009:  ret
    } // end of method Program::Main
    

    可以看到,声明引用类型和值类型的区别在于引用类型有一个newObj创建对象的操作。那么newObj到底做了哪些操作呢?主要操作如下:

    • 计算新建对象所需要的内存总数(包括基类的所有字段字节总数)。
    • 在前面所得字节总数的基础上再加上对象开销所需的字节数。开销包括:类型对象指针和同步块的索引。
    • CLR检查保留区域是否有足够的空间来存放新建对象。
      • 如果空间足够,调用类型的构造函数,将对象存放在NextObjPtr指向的内存地址中。
      • 如果空间不够,就执行一次垃圾回收来清理托管堆,如果依然不够,则抛出OutOfMemeryException异常
    • 最后,移动NextObjPtr指向托管堆下一个可用地址。
      可以看到,垃圾收集器通过检查托管堆上不再使用的对象来回收内存,那么垃圾收集器怎么确定对象是不再使用的对象呢?请接着往下看。

    1.2 应用程序的根

    每个应用程序都有一组根,一个根就是一个存储对象,其中包含一个指向引用类型的内存指针,它或者指向托管堆的对象,或者被设为null。如类字段、方法参数或者是局部变量都是根,注意只有引用类型才被认为是根,而值类型只是占用内存永远不会被认为为根。垃圾收集器是怎么工作的呢?工作主要分为以下两阶段:

    • 第一阶段,标记对象阶段。
      垃圾收集器开始执行的时候,首先假设托管堆中的对象都是可以收集的垃圾。它开始遍历线程的堆栈检查所有的根,如果发现根引用了一个对象那么就在该对象的同步块的索引字段上设置一位来标记它。同时检查该对象是否引用其他对象,如果引用则进行标记,通过递归的方式进行标记,直到发现根及根引用的对象已经标记,垃圾收集器将继续收集下一个根。

    • 第二阶段,压缩阶段。该阶段垃圾收集器线性的遍历堆以寻找包含未标记对象的连续区块。如果垃圾收集器找到了较小内存块,那么它忽略内存不计;如果找到了较大的连续内存块,那么垃圾收集器将把内存中非垃圾对象搬移到这些连续内存块中以压缩托管堆。


      图:垃圾收集器执行前的托管堆。

    对于以上描述,专业词汇较多不是蛮好理解。我们来举一个容易理解的例子:有一个执行清理房间的任务(任务方法),房间中有很多物品、柜子盒子及其里面的物品等都需要清理(对象清理),当我们执行这个任务时(调用方法),清理过程中我们可以标记物品,同时可能存在这样的情况,我们在清理其中一个盒子的时候,发现盒子里面还有其他的盒子,如手机盒子里面还有个装充电器的盒子(手机里面又引用了手机充电器的对象),那么我们需要深度遍历清理标记所有的盒子,遍历完成后,我们会发现,有很多以前有用现在无用的东西,如老式的手机充电器、数据线等;废旧的电池等(不在使用,不可达对象);这样我们会根据当时的情况将不再使用对象进行清理处理。而垃圾清理器大概就是做这样的工作,只是它处理的方式及细节更加复杂。

    1.3 对象的代

    当CLR试图寻找不可达对象的时候,它需要遍历托管堆上的对象。随着程序的运行,托管堆上的对象也越来越多,如果要对整个托管堆进行垃圾回收,那么会严重的影响性能。为了优化这个过程,CLR中使用了"代"的概念,托管堆上的每一个对象都被指定属于某个“代”(generation)。

    托管堆上的对象可以分为0、1、2三个代:

    • 0代:新构建的对象,垃圾收集器还没对它们执行任何检查
    • 1代:在一次垃圾收集清理没有被回收的对象
    • 2代:在至少两次垃圾收集清理没有被回收的对象。

    下面我们来看看CLR如何通过这种机制来优化垃圾收集机制的性能?
    图:垃圾收集代策略执行过程

    CLR初始化时,它会为每一代选择一个预算容量,假设为0代为256KB,1代为2M,2代为10M(实际可能不同),如果分配的新对象导致代容量超过预算容量,那么将执行垃圾收集清理操作。如上图所示:

    • 1、垃圾回收前,托管堆中对象ABCDE都处于第0代;
    • 2、假设ABCDE已占用256K内存,当需要创建新对象F时,开始执行垃圾回收,垃圾收集器判断CE为不可达对象,将对他们进行清理,完成后,对象ABD将变为1代对象;
    • 3、现在需创建FGHIJ对象,它们将都处于0代;这个时候1代对象中的B可能不再被调用变为不可达对象。这里面会发现:当0代对象内存不超过256KB时,垃圾回收器不会对1代对象进行检查清理,因此1代中不可达对象B在垃圾清理后依旧会保留在内存中。那么什么时候B会被清理呢?
    • 4、创建新对象,发现1代空间操作预算容量2M,这个时候将引发垃圾收集,回收不可达对象BH,同时原有1代对象AD变为2代,0代对象FGIJ变为1代。

    下面我们来通过事例代码验证上述的执行步骤:

    internal sealed class GenObj {
        ~GenObj()
        {
            Console.WriteLine("Finalize GenObj");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Maxnum gen:" + GC.MaxGeneration);
            //创建一个新的对象
            object o = new GenObj();
            //因为是新创建的对象,为0代
            Console.WriteLine("Gen "+GC.GetGeneration(o));
    
            //执行垃圾收集提升对象的代
            GC.Collect();
            Console.WriteLine("Gen " + GC.GetGeneration(o));
    
            //这里强制回收
            GC.Collect();
            Console.WriteLine("Gen " + GC.GetGeneration(o));
    
            GC.Collect();
            Console.WriteLine("Gen " + GC.GetGeneration(o));
    
            o = null;
            Console.WriteLine("Collecting Gen 0");
            GC.Collect(0);
            GC.WaitForPendingFinalizers();
    
            Console.WriteLine("Collecting Gen 0 1");
            GC.Collect(1);
            GC.WaitForPendingFinalizers();
    
            Console.WriteLine("Collecting Gen 0 1 2");
            GC.Collect(2);
            GC.WaitForPendingFinalizers();
    
            Console.ReadLine();
        }  
    }
    程序的返回结果为:
    Maxnum gen: 2
    Gen 0
    Gen 1
    Gen 2
    Gen 2
    Collecting Gen 0
    Collecting Gen 0 1
    Collecting Gen 0 1 2
    Finalize GenObj
    
    这里需要注意的时:
    GC.Collect()  强制对所有代码进行即时回收
    GC.Collect(int Generation) 强制对0代到指定代对象进行回收
    
    因此:我们将代码第二处的GC.Collect()修改为:GC.Collect(0) 
    程序的返回结果则变为:
    Maxnum gen: 2
    Gen 0
    Gen 1
    Gen 1     	//注意这里变为1 而不是2
    Gen 2
    Collecting Gen 0
    Collecting Gen 0 1
    Collecting Gen 0 1 2
    Finalize GenObj
    

    由此可以看出,垃圾回收机制通过引入代的机制,由遍历整个托管堆对象变成遍历少量的对象来达到性能优化的目的。当然,不仅如此,它还有其他的策略来进行性能优化。

    策略1:根据回收存货的比例高低来调整预算容量。如果垃圾收集器发现0代对象被收集以后存活下来的对象很少,它可能会决定将第0代的预算容量从256K减少到128K。已分配空间的减少意味着垃圾收集器执行的频率更高,但每次收集工作会减少,这样一来进程的工作集会变小;如果发现0代对象被手机以后存货下来的对象很多,也就是说没有回收较多的内存,那么可能决定将预算容量从256K增加到512K,这样回收的频率降低,执行回收的内存较多。

    策略2:大对象回收特殊机制。大对象是指任何占用内存等于或超过85000字节的对象,将会总被认为为2代对象。原因是:该堆中的对象的终结和内存释放和小对象相同,但是它永远不会被压缩。因为将85000字节的内存块搬移到堆中要浪费很多的CPU时间。

    对于这样的场景,我们来举一个更易懂的例子,一个公司业绩下滑,需要通过裁员来减清负担,开始时决定全公司范围裁员,结果搞的人心惶惶,人人自危,极大的影响了员工士气和工作效益(就好比遍历整个堆栈导致性能不佳);然后公司管理层决定优化这个方案,将裁员的人员定为刚进公司1年的新员工(0代),因为这样有一定的好处,不仅人少执行效率快,赔的钱少而且对业务影响也较小;经过这次风波后(资源清理),随着市场行情的提升,业绩越来越好,结果又持续招人(新对象创建),但是过了1年,由于XXX原因,效益大幅下降(内存、性能等下降),又要开始裁员(引发系统清理),规则还是按照上一次的规则,只是之前上一年没有裁掉的新员工(0代),他们在今年不再是一年级的新员工(上升为1代),在裁员的时候,发现只是裁新员工(部分表现不佳的)还不够,那么需要对去年新员工(1代)表现差的进行裁员(0代超过预算容量则开始检查1代)。这个例子可能不是很符合现实,但是它可以体现出代的思想。

    1.4 小结及启发

    通过上面的介绍,我们来回顾总结一下。我们可以学习到什么?我们大概能知道垃圾收集器是如何工作的,是如何高性能的工作的。我们不仅要会使用它,我们还需要知道它的原理是什么,这样当你遇到它,你就不会觉得它有多么的神秘,不仅如此,更重要的是,里面有很多思想是我们可以借鉴的,因为这些思想都是行内权威人士智慧的结晶。通过了解,我们还可以举一反三学习到:

    程序中根的标记递归算法,这不正是深度优先的算法吗?这个算法在很多场景都在使用,比如说搜索中的爬虫程序、图的遍历、最优最快路径等等应用场景都会用到;垃圾收集器通过引进“代”的概念来进行性能优化的策略原理。我们在项目的开发过程中,也有很多应用场景都可以借鉴这种思路来进行性能优化。比如说:现在很多大并发场景如12306、秒杀、购物网站等使用的排队机制,它们可以智能的设置队列容量,这不正是很好的体现吗?在比如说多级缓存系统系统缓存策略等等,这些应用场景都可以借鉴该思想。

    2、资源清理Finalize、Dispose、Using用法说明

    2.1 Finalize使用及原理说明

    终结(Finalization)是CLR提供的一种机制,他允许对象在垃圾回收其内存之前执行一些清理工作,回收它占用的资源(内存、本地资源等),当垃圾收集器判定一个对象为可收集垃圾时,它会通过该对象的Finalize方法来执行清理。C#中是通过在类名称前加一个波浪线~来定义的,这也就是我们所说的析构函数。通过ILDASM.EXE工具查看上面跟部分的GenObj类,打开确实可以发现Finalze方法。那么我们来看看Finalize的工作原理是什么?

    .method family hidebysig virtual instance void 
        Finalize() cil managed
    {
    	// Code size       25 (0x19)
    	.maxstack  1
    	.try
      	{
        IL_0000:  nop
        IL_0001:  ldstr      "Finalize GenObj"
        IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
        IL_000b:  nop
        IL_000c:  nop
        IL_000d:  leave.s    IL_0017
      }  // end .try
      finally
      {
        IL_000f:  ldarg.0
        IL_0010:  call       instance void [mscorlib]System.Object::Finalize()
        IL_0015:  nop
        IL_0016:  endfinally
      }  // end handler
      IL_0017:  nop
      IL_0018:  ret
    } // end of method GenObj::Finalize
    

    可以看到方法体的代码在Try中生成,而base.Finalize的调用则在finally中。通常Finalize方法的实现时调用Win32的CloseHandle函数,该函数接受本地资源的句柄作为参数。如:FileStream类定义了一个文件句柄字段来标示本地资源,同时也定了一个Finalize方法,该方法内部调用CloseHandler函数并为它传递文件句柄作为参数,确保托管堆的FileStream对象成为可收集垃圾之前,本地文件句柄可以得到关闭。C#中也提供给了相应的类来进行非托管资源的清理类:

    • CriticalFinalizerObject类型。

      它位于命名空间System.Runtime.ConstrainedExecution。CLR赋予它三个很酷的特征:1、首次构造派生于它的类型的任何对象,CLR立即对继承的层次结构中的所有Finalize方法进行JIT编译,这样在内存较小的情况下,不会影响Finalize方法因为没有内存而无法执行,从而导致资源泄露。2、CLR在调用了非派生自CriticalFinalizerObject该类的Finalize方法后再调用派生于CriticalFinalizerObject类型的对象的Finalize方法。这样可以确保拥有Finalize方法的托管资源类可以在Finalize方法中访问派生自CriticalFinalizerObject的对象。3、应用程序域被非法中断时,可以确保CLR调用派生自CriticalFinalizerObject类型的Finalize方法来执行资源清理。

    • SafeHandle类型及其派生类型。

    Microsoft意识到最常用的本地资源就是有Windows提供的资源,而且Windows资源都是由句柄操作。为了使便车简单,因此提供了SafeHandle类来提供资源句柄操作,它位于命名空间System.Runtime.InteropService.它本身派生于对于CriticalFinalizerObject类型,对于这个类的用法,具体可以查阅相关资料。

    我们来看看SafeHandle类都作了什么?

    [SecurityCritical, __DynamicallyInvokable, SecurityPermissio(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
    public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
    {
        // Fields
        private bool _fullyInitialized;
        private bool _ownsHandle;
        private int _state;
        [ForceTokenStabilization]
        protected IntPtr handle;
    
        // Methods
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle)
        {
            this.handle = invalidHandleValue;
            this._state = 4;
            this._ownsHandle = ownsHandle;
            if (!ownsHandle)
            {
                GC.SuppressFinalize(this);
            }
            this._fullyInitialized = true;
        }
    
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), SecurityCritical, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
        public void Close()
        {
            this.Dispose(true);
        }
    
        [MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecurityCritical, __DynamicallyInvokable]
        public extern void DangerousAddRef(ref bool success);
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public IntPtr DangerousGetHandle()
        {
            return this.handle;
        }
    
        [MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
        public extern void DangerousRelease();
        [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
        public void Dispose()
        {
            this.Dispose(true);
        }
    
        [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.InternalDispose();
            }
            else
            {
                this.InternalFinalize();
            }
        }
    
        [SecuritySafeCritical, __DynamicallyInvokable]
        ~SafeHandle()
        {
            this.Dispose(false);
        }
    
        [MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        private extern void InternalDispose();
        [MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        private extern void InternalFinalize();
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
        protected abstract bool ReleaseHandle();
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        protected void SetHandle(IntPtr handle)
        {
            this.handle = handle;
        }
    
        [MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
        public extern void SetHandleAsInvalid();
    
        // Properties
        [__DynamicallyInvokable]
        public bool IsClosed
        {
            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries"), __DynamicallyInvokable]
            get
            {
                return ((this._state & 1) == 1);
            }
        }
    
        [__DynamicallyInvokable]
        public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable] get; }
    	}
    

    首先可以看到,它继承于CriticalFinalizerObject对象,这样让它具备上面提到的三个特性;它同时实现了IDisposable接口。然后通过Dispose和close释放托管资源和非托管资源。其中提供了对本地资源句柄的操作,ReleaseHandle 如果在派生类中重写,执行释放句柄所需的代码。更多细节可以查看, https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.safehandle.aspx

    哪些时间会导致Finalize方法的调用呢?

    • 0代对象充满 该事件是目前导致垃圾回收执行最常见的一种方式。
    • 代码显式调用GC.Collect()方法 Micrsoft强烈建议不要这样干,但某些时候执行还是有意义的。
    • Windows报告内存不足
    • CLR卸载应用程序域
    • CLR被关闭

    由此可以看出Finalize方法的执行不能显式调用,因此它执行时间具备不确定性。

    2.2 Dispose使用

    Finalize方法非常有用,它可以确保托管对象在释放内存的同时不会泄露本地资源,但是它的问题在于我们不知道何时才会调用它。在使用本地资源的托管类型时,能够确定的释放或者是关闭对象都是很有用的。要提供确定释放或者关闭对象的能力,一个类型通常需要实现一种释放模式(DisposePattern).通过前面的SafeHandler类可以显式关闭本地资源,这是由于它实现了IDisposable接口。我们来看一下MSDN给出的Dispose释放写法。

    using System;
    
    class BaseClass : IDisposable
    {
    	//Flag: Has Dispose already been called?
    	bool disposed = false;
    
    	// Public implementation of Dispose pattern callable by consumers.
       public void Dispose()
       { 
          Dispose(true);
    	  //调用GC.SuppressFinalize(this)方法来阻止Finalize方法的调用
          GC.SuppressFinalize(this);           
       }
    
       // Protected implementation of Dispose pattern.
       protected virtual void Dispose(bool disposing)
       {
          if (disposed)
             return; 
    
          if (disposing) {
             // 释放托管资源
             //
          }
    
          // 释放非托管资源
    	
          //设置true 表示对象正在被显式的执行资源清理而不是垃圾收集器执行终结操作
          disposed = true;
       }
    }
    

    通过Dispose来释放资源,其实只是清理SafeHandle对象包装的资源方式之一。SafeHandle包装的资源清理还可以通过编程人员显式的调用Close、Dispose方法来清理;或者是通过垃圾收集器调用对象的Finalize方法来释放。上面给出的SafeHandle类代码实现,正好可以说明这一点。

    2.3 Using使用

    前面介绍了怎样显示的调用一个类型的Dispose或者是Close方法,如果决定显式调用,那么强烈建议把他们放在一个异常处理的finally代码块中,这样可以保证它们被执行。但是这样做书写的代码很是繁琐。为了解决这个问题,C#提供了一个using语句,它简化了上述finally的操作,并且能够得到和上述一样的效果。

    3、实例分析

    3.1 Windows服务关闭问题

    在很多时候,我们需要编写并开启一个Windows服务来执行需要循环执行的应用需求,在开启任务后,程序顺利执行;当关闭Windows服务时,发现服务关闭了,但是服务对应的资源线程还没有完全结束,要经过一段时间后,服务才会完全停止。这很有可能是因为,在服务Stop()方法里面,没有完全释放程序调用的资源导致的。

    3.2 Redis使用遇到的问题

    记得以前在使用Redis过程中遇到了一个奇怪的问题。

    先附上Redis帮助类:RedisManager.cs

    internal class RedisManager
    {
        private static readonly PooledRedisClientManager _Manager;
        static RedisManager()
        {
            _Manager = GetManager();
        }
        public static PooledRedisClientManager Manager
        {
            get
            {
                return _Manager;
            }
        }
        #region Help Methods
        private static PooledRedisClientManager GetManager()
        {
            var conn = ConfigurationManager.ConnectionStrings["Redis"].ConnectionString;
            if (string.IsNullOrEmpty(conn))
            {
                throw new Exception("请配置ConnectionString Key 为Redis的连接串");
            }
            var manager = new PooledRedisClientManager(conn);
            return manager;
        }
        #endregion
    }
    

    Dao.cs 数据操作基类

    public abstract class Dao<TEntity> : IDisposable where TEntity : class
    {
        private IRedisClient _Client;
        private IRedisTypedClient<TEntity> _Collection;
    
        public Dao()
        {
            _Client = RedisManager.Manager.GetClient();
            _Collection = _Client.As<TEntity>();
        }
        public void Save(TEntity entity)
        {
            _Collection.Store(entity);
        }
        public TEntity Get(object id)
        {
            return _Collection.GetById(id);
        }
        public void Delete(Object id)
        {
            _Collection.DeleteById(id);
        }
        #region Dispose
    	~Dao()
        {
            _Client.Dispose();
        }
    	/****以下为修改BUG时新加****/
        /*public void Close()
        {
            _Client.Dispose();
        }
        public void Dispose()
        {
            _Client.Dispose();
        }*/
        #endregion
    }
    

    单元测试用例:

    	[TestMethod]
        public void TestAddAndGet()
        {
            for (var i = 0; i < 10; i++)
            {
    			//Bug前代码
    			dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
                Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
    			//修复Bug代码
                /*using (var dao = new TestDao())
                {
                    dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
                    Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
                }*/
            }
        }
    

    运行单元测试,单条保存测试用例通过。但是在应用程序大量数据操作测试的时候,发现写入Redis的数据有丢失的情况,但并不是每次都会丢失。并且系统没有操作失败的异常日志。这个时候就感觉特别奇怪,于是就在单元测试时想办法重现这个错误,当按上面测试用例,循环10次保存数据测试用例通过,当循环100次时,发现异常重现了。没有异常抛出,测试用例也没有返回。然后设置断点调试,发现在运行第11次时,系统在_Client = RedisManager.Manager.GetClient()此处停住了。看了一下,代码中用到了PooledRedis ClientManager 客户端池对象,池对象有个特点是,有一个池的容量,当容量满的时候需要等待。而现在池中保存的就是RedisClient,是否是由于RedisClient达到了使用上限导致的。那么我们手动释放一下RedisClient是否解决这个错误,马上尝试了一下,通过使用Using显式释放资源,发现确实解决了问题。问题是解决了,但是我们不经要问?系统中不是有析构函数吗?难道这是PooledRedisClientManager池的一个Bug吗?带着这样的疑问,让我们来查看一下问题到底出现在哪里?

    1、析构函数只有在GC进行垃圾收集时才会被调用,而GC并不会马上执行,执行时间是不确定的。

    2、那么这到底是不是PooledRedisClientManager池的一个Bug呢?

    查看了一下对应的源码:

    	protected readonly int PoolSizeMultiplier = 10;
    	public IRedisClient GetClient()
        {
            lock (writeClients)
            {
                AssertValidReadWritePool();
    
                RedisClient inActiveClient;
                while ((inActiveClient = GetInActiveWriteClient()) == null)
                {
                    if (PoolTimeout.HasValue)
                    {
                        // wait for a connection, cry out if made to wait too long
                        if (!Monitor.Wait(writeClients, PoolTimeout.Value))
                            throw new TimeoutException(PoolTimeoutError);
                    }
                    else
                        Monitor.Wait(writeClients, RecheckPoolAfterMs);
                }
    
                WritePoolIndex++;
                inActiveClient.Active = true;
    
                InitClient(inActiveClient);
    
                return inActiveClient;
            }
        }
    

    从源代码可以看出,在获取可用GetInActiveWriteClient()为null时,有一个循环调用,线程一直等待获取可以用的RedisClient.当池中无可用的RedisClient对象时,那么线程将一直等待。对象池有一个特征:获取池中对象 → 使用对象 → 归还对象 。那么是否是由于使用完对象后没有归还对象呢?通过Using释放使用的对象切实可以起到归还的效果。于是再去挖掘程序中是否有Dispose或者是归还对象的操作, 发现该池定义了protected void Dispose(RedisClient redisClient)的方法,但是浏览源码切实没有发现任何地方显式调用这个Dispose。

    3.3 应用程序线程使用内存不断上升

    记得之前同事遇到这样的一个BUG,系统上线后,监控发现该应用程序使用内存不断上升,这个不得了,这意外着随着时间的持续,系统会因为内存不足导致应用挂掉。于是通过获取线上的DUMP文件,通过分析,发现char[] 数组的对象特别多,那么在什么时候我们会使用这么多的char[]呢?回顾一下,好像没有直接使用char[]的地方,但是我们知道,string对象经过编译后,它就是有char[]组成的,我们试着去找是否有StringBulider对象不停地加入数据,但是没有执行清理。结果真的发现有如此一个对象,这个对象据说是用来进行测试调试使用的,上线的时候应该去掉,结果上线的时候忘记了。

    参考资料

    《框架设计 CLR Via C#》 Jeffrey Richter著

    结束语:

    在技术学习的过程中,很多时候我们知其然不知其所以然,因此在开发的过程中可能遇到不知其所以然而导致的问题,到最后也无法找到问题的根本原因。我们需要深入了解原理,并且通过原理举一反三,在其他类似的应用场景可以借鉴他们的思想。

    写此文主要有三方面的目的:#####

    1、将学过的东西通过文字的形式分享出来,一直被分享,从未进行分享。- -

    2、有些时候,很多东西我们可能都理解,但是很难系统的书写出来,书写出来可以对已学知识和个人理解做一个记录和总结,进一步巩固已学知识。

    3、试着将比较枯燥的概念和理论通过更通俗易懂的例子解释出来,同时能够将这些枯燥难解的理论和实际结合,让知识体现的更加具体一点。

    个人感觉,文章还没有达到个人预期效果。其主要表现在如下两方面:其一,对于细节的理解可能说的不够透彻,没有找到通俗易懂的例子来进行解释;其二,对于项目中遇到的关于垃圾收集典型问题所举例子还不够丰富,没有真正体现出核心的价值。因此欢迎各位博友能够分享个人经验进行补充,让对此方面知识还不是十分了解的同学更容易理解。

    PS:初次写,清大家多多关照以示鼓励。……

    ——找钢网搜索引擎开发部 周明

  • 相关阅读:
    js中this指向的三种情况
    js 对象克隆方法总结(不改变原对象)
    JS 数组克隆方法总结(不可更改原数组)
    第七章、函数的基础之函数体系01
    第六篇、文件处理之文件修改的两种方式
    第六篇、文件处理之文件的高级应用
    第六篇.文件处理之python2和3字符编码的区别
    第六篇、文件处理之字符编码
    第五篇python进阶之深浅拷贝
    jquery的insertBefore(),insertAfter(),after(),before()
  • 原文地址:https://www.cnblogs.com/izhaogang/p/collector.html
Copyright © 2011-2022 走看看