3D Game Programming & Design

An Introduction to Game Engine and OO Design Patterns

face

View the Project on GitHub

附录 X3-08、托管资源与垃圾回收

1、预备知识

1.1 程序的堆空间与栈空间

由C/C++编译的程序,其进程存储空间分为以下几个部分:

1、 栈(stack)区是作为执行线程的临时空间预留的内存。调用函数时, 在堆栈顶部为局部变量和一些簿记数据预留块。当该函数返回时, 该块将变得不使用, 并且可以在下次调用函数时使用。堆栈始终在后进先出 (LIFO) 顺序中保留。最近保留的块总是下一个要释放的块。这使得跟踪栈变得非常简单;从堆栈中释放块(函数的参数值,局部变量的值)只不过是调整一个指针。

2、堆(heap)区:堆是预留给动态分配的内存。与堆栈不同, 不存在从堆中分配和释放块的强制模式。您可以随时分配块, 并在任何时候释放它。这使得在任何给定时间跟踪堆中的哪些部分被分配或释放是非常复杂的;有许多自定义堆分配器可用于为不同的使用模式调整堆性能。程序员必须正确的使用堆,即每次分配的块必须在使用完毕后释放,否则则会出现内存泄漏。

3、全局区(static):也叫静态数据内存空间,存储全局变量和静态变量,全局变量和静态变量的存储是放一块的,初始化的全局变量和静态变量放一块区域,没有初始化的在相邻的另一块区域,程序结束后由系统释放。

4、文字常量区:常量字符串放在这里,程序结束后由系统释放。

5、程序代码区:存放函数体的二进制代码。

每个线程都得到一个堆栈,而应用程序通常只有一个堆(尽管不同类型的分配有多个堆并不少见)。

更多详细信息,建议参考 What and where are the stack and heap?, 这些都是常见面试题:

1.2 对象、结构与数组的内存资源放置

对象:

结构:

数组:

1.3 垃圾回收

手动内存空间回收产生的常见问题包括:

为了帮助初级程序员回避这些复杂的 bug,从 Basic,Logo 等语言开始,人们尝试跟踪存储空间的使用,在编译期与运行期配合,实现自动空间回收的机制与算法。到 Java 语言出现,自动回收机制日益成熟,Java 虚拟机通过引用计数机制,自动管理内存。常见的垃圾回收策略包括:跟踪、引用计数、Escape analysis

以 Java 垃圾回收(GC/Garbage Collection)为例,如果 myObject = nil 赋值后,如果 myObject 原来分配的对象不存在其他引用(对象不可达),垃圾回收管理程序会自动调用 Object.finalize 方法释放原对象引用的其他对象资源,但不包括操作系统的一些句柄,如文件、图像刷(需要用户程序自己关闭)等。因为 JDK 规范定义了一个对象的 finalize 能且仅能调用一次,因此不建议用户重写该方法,以避免影响垃圾回收机制正常工作。然后 GC 再检查是否能执行该对象的析构方法并回收该对象空间。更多垃圾回收信息 Java Garbage Collection Basics

2、C# 托管机制

C# 把内存资源分为两大类:

在.NET中,Object.Finalize()方法是无法重载的,编译器是根据类自动生成Object.Finalize()方法,用来释放托管的资源。

注意,不能在析构函数中释放托管资源,因为析构函数是有垃圾回收器调用的,可能在析构函数调用之前,类包含的托管资源已经被回收了,从而导致无法预知的结果

Microsoft为非托管资源的回收专门定义了一个接口:IDisposable,接口中只包含一个Dispose()方法。任何需要在对象析构之前释放非托管资源的类,都应该实现此接口。

3、非托管资源与互操作

为了与操作系统或其他应用互操作,不可避免需要使用指针。核心问题是,如果指针被其它应用保存,该指针返回时,而其指向的内容有可能已经被 .Net 自动回收了。这种情况下,要么指针指向内容被保存在非托管空间,要么强制不被垃圾回收器回收!

3.1 C# 互操作常见对象

3.2 值类型(struct)互操作

由于数值类型在栈上,当变量出了作用域就可能被回收了。

Marshal.PtrToStructure Method 给出了标准的互操作案例:

using System;
using System.Runtime.InteropServices;

public struct Point
{
    public int x;
    public int y;
}

class Example
{
    static void Main()
    {
        // Create a point struct.
        Point p;
        p.x = 1;
        p.y = 1;

        Console.WriteLine("The value of first point is " + p.x + " and " + p.y + ".");

        // Initialize unmanged memory to hold the struct.
        IntPtr pnt = Marshal.AllocHGlobal(Marshal.SizeOf(p));

        try
        {
            // Copy the struct to unmanaged memory.
            Marshal.StructureToPtr(p, pnt, false);

            // do something here

            // Create another point.
            Point anotherP;
            // Set this Point to the value of the 
            // Point in unmanaged memory. 
            anotherP = (Point)Marshal.PtrToStructure(pnt, typeof(Point));

            Console.WriteLine("The value of new point is " + anotherP.x + " and " + anotherP.y + ".");

        }
        finally
        {
            // Free the unmanaged memory.
            Marshal.FreeHGlobal(pnt);
        }
    }
}

lua 互操作参见正文第十四章 Vector3 案例

3.3 对象(Object)互操作

对象在堆中,它可能被 GC 整理,修改了位置,也可能被释放。因此需要需要将它的一个指针与一个 handle 绑定。由于有一个指针引用,所以它不会释放。即使被 GC 修改了存储位置,用 handle 查的指针依然是正确的。

GCHandle.Alloc Method (Object) MSDN 官方案例:

public delegate bool CallBack(int handle, IntPtr param);

public class LibWrap
{
	// passing managed object as LPARAM
	// BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);

	[DllImport("user32.dll")]
	public static extern bool EnumWindows(CallBack cb, IntPtr param);
}

public class App
{
	public static void Main()
	{
		Run();
	}

        [SecurityPermission(SecurityAction.Demand, UnmanagedCode=true)]
	public static void Run()
        {
		TextWriter tw = System.Console.Out;
        //钉住对象 tw
		GCHandle gch = GCHandle.Alloc(tw);

		CallBack cewp = new CallBack(CaptureEnumWindowsProc);

		// platform invoke will prevent delegate to be garbage collected
		// before call ends
		LibWrap.EnumWindows(cewp, GCHandle.ToIntPtr(gch));
		gch.Free();
        }

	private static bool CaptureEnumWindowsProc(int handle, IntPtr param)
	{
		GCHandle gch = GCHandle.FromIntPtr(param);
		TextWriter tw = (TextWriter)gch.Target;
		tw.WriteLine(handle);
		return true;
	}
}

lua 互操作参见正文第十四章 GameObject 案例。

注:官方也有 bug 的! gch.Free() 如果先执行了,TextWriter tw = (TextWriter)gch.Target; 就有可能查不到 Target 对象了。当然,这个案例对的。