概念1:内存自动管理, 1、提高了软件开发的抽象度;2、程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题;3、可以使模块的接口更加的清晰,减小模块间的偶合; 4、大大减少了内存人为管理不当所带来的Bug;5、使内存管理更加高效。可以使程序员可以从复杂的内存问题中摆脱出来,从而提高了软件开发的速度、质量和安全性.
2:GC,Garbage Collector(垃圾收集器,在不至于混淆的情况下也成为GC)以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象[2],通过识别它们是否被引用来确定哪些对象是已经死亡的、哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC工作的原理。为了实现这个原理,GC有多种算法。比较常见的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虚拟系统.NET CLR,Java VM和Rotor都是采用的Mark Sweep算法。
3: .NET GC相关概念 CLR:公共运行时环境,管理托管堆。CTR:设置引用类型,值类型。GC:只负责回收托管对象,不负责回收非托管对象。
GC算法
一Mark-Compact算法: .NET中支撑GC回收机制的是Mark-Compact算法,阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2: Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎片整理。
Heap内存经过回收、压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起→确定roots→创建reachable objects graph→对象回收→heap压缩→指针(NextObjPtr,总是指向最后一个对象之后的位置)修复。可以这样理解roots:heap中对象的引用关系错综复杂(交叉引用、循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口点,所有的全局和静态对象指针是应用程序的根对象,另外在线程栈上的局部变量/参数也是应用程序的根对象,还有CPU寄存器中的指向托管堆的对象也是根对象。
GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register) 。 Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。例如当前执行函数的局部变量对象A是一个root object,他的成员变量引用了对象B,则B是一个reachable object。从roots出发可以创建reachable objects graph,剩余对象即为unreachable,可以被回收 。
指针修复是因为compact过程移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack、CPU register中的指针以及heap中其他对象的引用指针。Debug和release执行模式之间稍有区别,release模式下后续代码没有引用的对象是unreachable的,而debug模式下需要等到当前函数执行完毕,这些对象才会成为unreachable,目的是为了调试时跟踪局部对象的内容。传给了COM+的托管对象也会成为root,并且具有一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动位置的对象,例如传递给非托管代码的对象(或者使用了fixed关键字),GC在指针修复时无法修改非托管代码中的引用指针,因此将这些对象移动将发生异常。pinned objects会导致heap出现碎片,但大部分情况来说传给非托管代码的对象应当在GC时能够被回收掉。
二、 Generational 分代算法:程序可能使用几百M、几G的内存,对这样的内存区域进行GC操作成本很高,分代算法具备一定统计学基础,对GC的性能改善效果比较明显。将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉。分代算法的假设前提条件:
1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长;
2、对部分内存进行回收比基于全部内存的回收操作要快;
3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率,.NET将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2;
三、Finalization Queue和Freachable Queue:这两个队列和.NET对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。 .NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生。
托管资源:
.NET中的所有类型都是(直接或间接)从System.Object类型派生的。
CTS中的类型被分成两大类——引用类型(reference type,又叫托管类型[managed type]),分配在内存堆上;值类型(value type),分配在堆栈上。
值类型在栈里,先进后出,值类型变量的生命有先后顺序,这个确保了值类型变量在退出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆(Managed Heap)上,声明一个变量在栈上保存,当使用new创建对象时,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存.
.NET中超过80%的资源都是托管资源。
非托管资源:
ApplicationContext, Brush, Component, ComponentDesigner, Container, Context, Cursor, FileStream, Font, Icon, Image, Matrix, Object, OdbcDataReader, OleDBDataReader, Pen, Regex, Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源。(应用程序上下文、画笔、组件、组件设计器、容器、上下文、光标、文件流、字体、图标、图像、矩阵、对象、OdbcDataReader、OleDBDataReader、钢笔、正则表达式、套接字、StreamWriter、定时器、工具提示,文件句柄, GDI资源, 数据库连接等等资源)
GC.Collect() 方法:强制进行垃圾回收。Collect()强制对所有代进行即时垃圾回收。Collect(Int32)强制对零代到指定代进行即时垃圾回收.Collect(Int32, GCCollectionMode)强制在 GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收.
GC垃圾回收的时机与条件:1系统具有低的物理内存。2由托管堆上已分配的对象使用的内存超出了可接受的阈值(即将涉及到代的概念)。随着进程的运行,此阈值会不断地进行调整。3,强制调用 GC.Collect 方法。4,CLR正在卸载应用程序域(AppDomain)5,CLR正在关闭。
C# NEW 操作符:A.它计算类型以及所有基类型(一直到System.Object,虽然它没有定义自己的实例字段)中定义的所有实例字段需要的字节数。堆上的每个对象都需要一些额外的成员---即“类型对象指针”和“同步块索引”。 这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。B.它从托管堆中分配制定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0.C.它初始化对象的“类型对象指针”和“同步块索引”成员。D.调用类型的实力构造器,向其传入在对new的调用中指定的任何实参。大多数编译器都在构造器重自动生成代码来调用一个基类构造。每个类型的构造在调用时,都要负责初始化由这个类型定义的实例字段。最终调用的说System.Object的构造器,该构造器知识简单地返回,不会做其他任何事情。为了证明这一点,可使用ILDasm.exe加载MSCorLib.dll,检查System.Object的构造器。简单一点就是分配初始化为0的内存,调用对象构造器,维护这个对象的指针和索引块.
注意在.NET中:
1:GC并不是能释放所有的资源。它不能自动释放非托管资源。
2:GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。使用using语句可以简化资源管理。using (obj){}
3、GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法。
4、GC在一个独立的线程中运行来删除不再被引用的内存。
5、GC每次运行时会压缩托管堆。
6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。
7、对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数
8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer,不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer,而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。
9、.NET GC使用"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。
10、GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。
11、只管理内存,非托管资源,如文件句柄,GDI资源,数据库连接等还需要用户去管理。
12、循环引用,网状结构等的实现会变得简单。GC的标志-压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。
13,对于未装箱的值类型对象而言,由于其不在堆上分配,一旦定义了该类型的一个实例的方法不再活动,为它们分配的存储资源就会被释放,而不是等着进行垃圾回收.
14,将引用赋值为null并不意味着强制GC立即启动并把对象从堆上移除,唯一完成的事情是显式取消了引用和之前 引用所指向对象之间的连接。
15,using语句只适用于那些实现了IDisposable接口的类型.
上面说完了windows平台下的GC机制,下面说说unity3d下如何做优化:
目的:在游戏中需要减少触发GC的条件,因为会耗费CPU资源.
首先我们要知道所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 明白了这一点,你也就明白了GC不是用来处理引擎的Assets(贴图,音效,模型等等)的内存释放的,因为U3D引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。
1,老生常谈的string,在字符串过多或者经常使用同一字符串进行 + 操作,需要换成StringBuilder.string在两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收.StringBuilder则不会生成一个新的实例对象.
2,尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
3,不要直接访问gameobject的tag属性。比如if (go.tag == “human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。
4,不要经常性的创建销毁对象,需要做一个对象管理池,复杂一些的就是生产者消费者模式.简单点的就是3个对象来回复用.
5,不使用LINQ命令,因为它们一般会分配中间缓器,而这很容易生成垃圾内存。
6,C#与C++交互的时候,①由C#创建内存,传入C++函数:托管内存转为非托管内存,以及操作非托管内存的时候,需要特别精细的控制.GCHandle gch = GCHandle.Alloc(...)标记某块内存地址不会被GC,并且固定其内存地址(不会被 堆 移动压缩),表示托管内存转化为非托管内存.调用gch.Free();函数,表示非托管内存转化为托管内存.② 由C++创建内存,回调到C#函数:使用IntPtr.Zero作为指针类型,配合out参数,使用Marshal.Copy(...)函数将非托管内存转化为托管内存在C#中使用,使用Marshal.FreeHGlobal(...)释放非托管内存.③在C#中申请一块非托管内存给C++使用,调用Marshal.AllocHGlobal(..)函数.
ps:unity3d中,场景切换之后,上一个场景都会被垃圾回收器回收.