复制(分代)垃圾收集提供了任何形式的自动内存管理的最佳性能,但需要修复指向重定位数据块的指针。在支持这种内存管理技术的语言中,这是通过禁止指针算术并确保所有指针都指向可识别对象的开头来启用的。
如果您在运行时使用 JIT 编译器生成代码,事情看起来有点棘手,因为调用堆栈上的返回地址将指向,而不是代码块的开头,而是其中的随机位置,因此修复是一个问题。
这通常是如何解决的?
复制(分代)垃圾收集提供了任何形式的自动内存管理的最佳性能,但需要修复指向重定位数据块的指针。在支持这种内存管理技术的语言中,这是通过禁止指针算术并确保所有指针都指向可识别对象的开头来启用的。
如果您在运行时使用 JIT 编译器生成代码,事情看起来有点棘手,因为调用堆栈上的返回地址将指向,而不是代码块的开头,而是其中的随机位置,因此修复是一个问题。
这通常是如何解决的?
很多时候,您不会重新定位代码。这既是因为修复堆栈和其他地址确实很复杂(想想跨代码片段的跳转),而且因为您实际上不需要对此类代码进行垃圾收集(因为它仅由您编写的代码操作,所以您可以做手动内存管理)。您也不希望创建大量机器代码(与应用程序对象相比),因此碎片等不是问题。
如果您坚持移动机器代码并修复堆栈,我认为有一种方法:类似于Mark-Compact,构建一个“break table”(我不知道这个名字来自哪里;“relocation table”可能是更清晰),它告诉您应该调整指向移动对象的指针的数量。现在,遍历堆栈以获取返回地址(当然,高度特定于平台)并在它们引用重定位代码时修复它们。与其寻找完全匹配,不如寻找低于您当前要替换的返回地址的最高地址。您可以通过查看对象大小来检查该地址是否确实引用了某些移动的机器代码(毕竟,您有一个指向对象开头的指针)。由于各种原因,这种方法并非对所有对象都可行。
不过,还有其他原因可以做类似的事情。一些 JIT 编译器具有堆栈替换功能,这意味着创建一些机器代码的新版本(例如,优化程度更高或优化程度更低)并用它替换所有出现的旧版本。不过,这比仅修复返回地址要复杂得多。您必须确保新版本在逻辑上继续旧版本挂起的地方。我不熟悉这是如何实现的,所以我不会详细介绍。