C#内存管理-栈堆/回收器托管/非托管资源释放/指针的应用

1.栈内存-从上往下增长,释放时从下部的末尾出栈

.net 5中栈好像也是从下往上增长了,释放时从上部顶端出栈。
栈类型数据,整体是从进程空间中的栈内存资源的最大地址处开始分配栈内存的,栈指针总是指向已用地址更小的内存地址上,下一个为空闲内存地址。当变量出了作用域时候,栈内存就会释放,栈指针上移到非空闲地址的小地址上,若再进来变量就会覆盖之前不用了的地址内存区域。
     栈内存分配是对整个变量来说的,具体变量内部十六进制数据排序主机中基本是小端模式低地址在低内存地址中。栈上变量或结构体生命期是嵌套的,栈中大小是有限制的。

2.托管堆-从下往上增长,释放时是按需进行的

堆是托管堆,可以分配更大更易于手动控制的内存,堆是一开始在低内存地址的,上层的空间才是空闲的,向上生长的和栈刚好相反,引用对象指向堆中地址的低地址。一个堆内存有一个栈引用持有,当栈引用超过生命期的时候,堆内存减少引用计数,垃圾回收器会隔一段时间扫描下堆内存将引用计数为0的堆内存清理掉。
   堆内存分配也是对整个对象来说的,具体对象内部数据排序十六进制排序主机是小端模式数据的低位存放在低地址。堆中对象生命期看引用的生命期还有回收器回收周期来决定,堆内存可以获得比较大的易于控制的内存空间。

3.垃圾回收器工作方式 -会重新挪动整理和修改对象的引用地址/System.GC.Collect()

最近使用最多和大堆不放置总堆中提高性能,System.GC.Collect()手动调用垃圾回收,不保证一次回收完但是也很有作用了。
在CC++的旧堆中,如果删除了不需要的内存,那么堆内存区域中就会存在分散的非空闲和空闲内存块,运行越久分配越频繁,那么产生的内存碎片越多,每次申请新的内存空间都要遍历整个堆。但是C#CRL托管堆中垃圾回收器每次扫描释放完一次托管堆资源,就会把其它还在生命期内的对象移动会堆的底部(因为是从下往上增长的),整理一遍(总的小堆内存中,大堆资源不会移动整理),再次形成连续的内存块,垃圾回收器会将这些对象的所有引用都用新的地址来更新,垃圾回收器的这个整理堆内存操作是托管堆和非托管的旧堆的区别所在。尽管压缩堆,修改它移动的所有对象引用,致使性能降低,但是.net中实例化对象申请内存空间和访问速度都会比较快,.net认为压缩堆是值得的。
垃圾回收器在.net CRL认为需要调用它时候才调用,但是也可以通过手动调用System.GC.Collect()方法强迫回收器在代码中运行,例如一下子取消很多对象的引用,例如换关卡时候,就适合调用,但是垃圾回收器不能保证在一次垃圾回收过程中,将所有未引用的对象都从堆中删除。
.net垃圾回收运行时候,应用程序不能继续运行,因此会影响性能。但是.net垃圾回收使用了一些算法和架构设计来提高性能,算法是指FIFO原则,刚进来的对象肯定很多很快被回收,所以.net托管堆是往上增长的,最新的总是放置在最上面,删除得也多,所以降低了托管堆移动的概率。有趣的是,在给对象分配内存空间时,如果超出第0代(最上层区域)对应的部分容量,或者调用了GC.Collect()方法,就会进行垃圾回收。
架构是说大堆资源85000Byte,83kb的堆资源那么会放置在特殊大堆对象的堆上,而不是总堆中因此不会挪动大堆对象压缩,因为大堆对象挪动比较耗时,因此提高了垃圾回收的性能。

4.释放非托管的资源 IDisposable和析构函数

例如:
1)非托管的堆内存
2)文件句柄
3)网络连接
4)数据库连接
那么有两种机制可以释放非托管的资源,这两种机制为问题提供了两种略不同的解决方法,所以一般都要一起实现,两种机制是:
1)声明一个析构函数(或终结器)作为类的一个成员,不确定和推迟执行,但一定执行
析构函数或终结器:
析构函数和C++写法一样,会在CRL中用基类的Finalize()方法来调用,调用过程是:
protected override void Finalize()
{
 try
 {
     // destructor implementation
 }
 finally
 {
  base.Finalize(); // 最终一定会执行
 }
}
所以可以重写类的析构函数,或重写Finalize()函数。
.net CRL对析构或者终结函数调用有两个严重的问题:
一个是对象销毁时候析构函数并不能立即执行,因为C#使用垃圾回收器的工作方式。
二是如果定义了析构或终结函数,那么会延迟对象删除时间,没有析构的对象在一次垃圾回收中会被处理掉,有析构或终结重定义的函数需要两次才能删除掉。运行库使用一个线程来执行所有对象的Finalize()方法,如果频繁使用析构而且执行长时间的清理任务,会比较影响性能。

2)在类中实现System.IDisposable接口立即执行,但要手动调用Dispose()函数
在C#中,推荐使用System.IDisposable接口来替代析构函数,派生自System.IDisposable接口,那么就可以解除对象销毁和垃圾回收器的固有关系,对象销毁时候可以马上调用该接口的Dispose函数。实现方法是:
class ResourceGobbler: IDisposable
{
 public void Dispose()
 {
 // implementation
 }
}
调用时候,为了避免使用资源过程中出现异常,可以使用:
ResourceGobbler theInstance = null;
try
{
 theInstance = new ResourceGobbler();
 // do your processing
}
finally
{
 if(theIntance != null)
 {
  theInstance.Dispose();
 }
}

C#为了使得释放非托管堆资源时候更加方便和明显,提供了using语句来实现上述类似的功能,当对象超出作用域时候会自动调用Dispose函数:
using(ResourceGobbler theInstance = new ResourceGobbler())
{
// do your processing
}
对于文件,数据库连接,网络连接,可以用Close方法封装Dispose()方法,更加符合释放逻辑。
3)综合上面两个方法,应该利用 System.IDisposable接口立即执行且手动调用,但也要提供析构或Finalize()函数确保一定执行。
using System;
public class ResourceHolder : IDisposable
{
  // 标识非托管资源是否清理了
   private bool isDisposed = false;

   public void Dispose() 
   {
      Dispose(true);
      // 1.通知GC这个类没有析构函数,不需要调用析构函数,可以提高性能
      GC.SuppressFinalize(this); 
   }

   // 2.无论是Dispose()还是析构函数调用,都封装到一个函数中,方便统一处理。
   // bool disposing标识是Dispose()调用,还是析构函数调用的
   protected virtual void Dispose(bool disposing) 
   {
     // 没有清理过才清理
      if (!isDisposed) 
      {
         //3. Dispose()调用,可以释放托管堆上的对象引用的非托管资源
         if (disposing) 
         {
            // Cleanup managed objects by calling their 
            // Dispose() methods.
         }
         // 如果不是Dispose()调用, 只是负责自己直接引用的非托管堆资源
          // 因为垃圾回收器调用不能确定引用的其它对象是否释放了,所以不能再去调用其它对象的Dispose()了。
         // Cleanup unmanaged objects
      }
      isDisposed = true;
   }

   ~ResourceHolder()
   {
      Dispose (false);
   }

   public void SomeMethod() 
   {
      // Ensure object not already disposed before execution of any method
      if(isDisposed) 
      {
         throw new ObjectDisposedException("ResourceHolder");
      }
      // 4.没有释放非托管堆资源才调用
      // method implementation…
   }
}

5.指针直接访问内存

为了访问.net不支持的系统级别代码和组件,但很多情况下可以使用DllImport声明,避免使用指针,例如:System.InPtr类。
为了性能提高,指针可以提供最高效的方式访问和处理数据,但是更多性能问题是多线程问题,程序逻辑问题,除非特别要求才需要使用指针。
但是使用指针是有代价的,需要非常高的编程技巧和很强的能力,需要仔细考虑代码所完成的逻辑操作。如果不仔细,使用指针就很容易在程序中引入细微的,难以查找的错误,例如:重写其它变量导致栈溢出,空指针,野指针的产生,堆内存丢失问题,内存泄露问题,指针越界和强制转换问题,访问某些没有存储变量的内存区域,甚至重写.NET运行库所需要的代码信息而导致程序崩溃。
如果使用指针,就必须授予代码运行库的代码访问安全机制的高级别信任,否则就不能执行它。.net默认的访问安全策略中,只有运行在本地计算机才是可以的,远程的需要给代码授予额外的许可代码才可以执行。
1)使用unsafe关键字编写不安全的代码
unsafe可以修饰类,函数成员,类数据成员,函数的参数;但是不能放置在函数的局部变量中。
使用unsafe还需要指定编译选项命令行是:csc /unsafe source.cs或者改变编译器的项目属性Build选项中的指定。
2)C#中指针的使用和指针运算
C#中int *pX, pY;等价于C++中的int *pX, *pY;因为C#类型声明是和类型相关的,而不是和变量相关的。
&取址运算符,*取地址上的内容,和C++一样的。
指针只是一个整型变量,指针上面的数据是一个地址,所以对p地址上的数据*p操作,就会得到指针指向的地址上面的值。
sizeof可以确定值类型数据的字节大小,不能将sizeof应用于类。
void*类型指针不能做算术运算,如果是明确类型的指针,做++,--操作那么指针会移动该类型一个单位的大小。
给类型为T的指针加上数值X,其中指针的值为P,则得到的结果是 P + X*sizeof(T);
例如:Int*p = n; p= p + 4,p会移动4*4 = 16个字节的位置。减去1也是减去一个类型单位大小。
3)指针(非托管)和垃圾回收器(托管类型)
C#指针可以指向基础类型和结构体,但是不能指向一个类或数组,如果需要指向类或者数组那么会破坏.net中的垃圾回收器维护的与类相关的信息,因为垃圾回收器维护的是托管类型,指针是非托管类型,垃圾回收器将不能处理他们。
4)指针的类型转换
指针上面数据的类型转换要非常小心,如果指针指向的类型比较大,强转给的类型比较小,那么就会出现栈溢出。
如果指针指向的类型比较小,强转给的类型比较大,那么强转给的类型会填充其它内存区域的值,得不到正确结果。
指针类型强转时候用checked关键字也不能检测到这样的溢出错误,因为.net遇到指针认为这种错误是由程序员保证的。
5)结构指针
结构中不能有引用类型(因为会破坏托管堆资源),使用和C++一样只是结构体声明时候为:
MyStruct struct = new MyStruct();
pStruct = &struct; // struct是栈变量,取得栈变量的地址
6)类成员指针
指针不能指向类对象,因为垃圾回收器管理的托管堆资源会失效。
但是指针可以指向类的值类型数据,如果不做任何修饰直接指向类值类型成员的地址,那么会导致垃圾回收器重新整理堆内存时候将对象的引用挪动到了新的位置,而导致指针变为野指针。所以C#针对这种情况定义了fixed关键字,表示垃圾回收器不要挪动这个对象和它内部的数据。
例如:
using System;

namespace PointerPlayground2
{
    internal class Program
    {
        private static unsafe void Main()
        {
            Console.WriteLine("Size of Currency struct is " + sizeof(CurrencyStruct));
            CurrencyStruct amount1, amount2;
            CurrencyStruct* pAmount = &amount1;
            long* pDollars = &(pAmount->Dollars);
            byte* pCents = &(pAmount->Cents);

            Console.WriteLine("Address of amount1 is 0x{0:X}", (uint)&amount1);
            Console.WriteLine("Address of amount2 is 0x{0:X}", (uint)&amount2);
            Console.WriteLine("Address of pAmt is 0x{0:X}", (uint)&pAmount);
            Console.WriteLine("Address of pDollars is 0x{0:X}", (uint)&pDollars);
            Console.WriteLine("Address of pCents is 0x{0:X}", (uint)&pCents);
            pAmount->Dollars = 20;
            *pCents = 50;
            Console.WriteLine("amount1 contains " + amount1);
            --pAmount;   // this should get it to point to amount2
            Console.WriteLine("amount2 has address 0x{0:X} and contains {1}",
               (uint)pAmount, *pAmount);
            // do some clever casting to get pCents to point to cents
            // inside amount2
            CurrencyStruct* pTempCurrency = (CurrencyStruct*)pCents;
            pCents = (byte*)(--pTempCurrency);
            Console.WriteLine("Address of pCents is now 0x{0:X}", (uint)&pCents);
            Console.WriteLine("\nNow with classes");
            // now try it out with classes
            CurrencyClass amount3 = new CurrencyClass();

            fixed (long* pDollars2 = &(amount3.Dollars))
            fixed (byte* pCents2 = &(amount3.Cents))
            {
                Console.WriteLine("amount3.Dollars has address 0x{0:X}", (uint)pDollars2);
                Console.WriteLine("amount3.Cents has address 0x{0:X}", (uint)pCents2);
                *pDollars2 = -100;
                Console.WriteLine("amount3 contains " + amount3);
            }
        }
    }

    internal struct CurrencyStruct
    {
        public long Dollars;
        public byte Cents;

        public override string ToString()
        {
            return "$" + Dollars + "." + Cents;
        }
    }

    internal class CurrencyClass
    {
        public long Dollars;
        public byte Cents;

        public override string ToString()
        {
            return "$" + Dollars + "." + Cents;
        }
    }
}

6.指针应用

因为数组和字符串都是存储在托管堆中的,性能一般,为了避免数组中引用对象的系统开销,可以利用指针来实现高性能的数组。
创建一个高性能数组需要另一个关键字:
stackallo指定.net运行库在栈上分配一定量的内存。
实例:
using System;
namespace QuickArray
{
    internal class Program
    {
        private static unsafe void Main()
        {
            Console.Write("How big an array do you want? \n> ");
            string userInput = Console.ReadLine();
            uint size = uint.Parse(userInput);
            // 用stackalloc向.net CRL申请栈内存空间,但是没有初始化值,这样效率更高
            // stackalloc要求类型的个数是(int) 类型,字节数=sizeof(T)*count.
            // 返回类型必须是指针类型,可以用指针运算来操作内存中的数据,迭代和赋值
            // C#中指针访问:*(p+i)等价于p[i],和C/C++一致。
            // 高性能数组要注意数组越界,指针指向了不确定的内存区域
            long* pArray = stackalloc long[(int)size];
            for (int i = 0; i < size; i++)
            {
                pArray[i] = i*i;
            }

            for (int i = 0; i < size; i++)
            {
                Console.WriteLine("Element {0} = {1}", i, *(pArray + i));
            }

            Console.ReadLine();
        }
    }
}

你可能感兴趣的:(C#内存管理-栈堆/回收器托管/非托管资源释放/指针的应用)