现代垃圾收集器(如在 CLR、JVM 中)使用哪些技术来判断从堆栈中引用了哪些堆对象?
具体来说,VM 如何从知道堆栈从哪里开始解释所有对堆对象的本地引用返回?
现代垃圾收集器(如在 CLR、JVM 中)使用哪些技术来判断从堆栈中引用了哪些堆对象?
具体来说,VM 如何从知道堆栈从哪里开始解释所有对堆对象的本地引用返回?
在 Java 中(可能在 CLR 中,尽管我不太了解它的内部结构),字节码是用对象与原始信息输入的。因此,字节码中有数据结构来描述每个堆栈帧中的哪些变量是对象,哪些是原语。当 GC 需要扫描根集时,它使用这些StackMapTables来区分引用和非引用。
CLR 和 Java 必须有这样的机制,因为它们是精确的收集器。有一些保守的收集器,比如boehm 收集器,它们将堆栈上的每个偏移量都视为一个可能的指针。他们查看该值(当被视为指针时)是否是堆中的偏移量,如果是,则将其标记为活动的。
看看 1996 年 8 月的这篇Artima文章,Java 的垃圾收集堆;特别是第 2 页。
任何垃圾收集算法都必须做两件基本的事情。首先,它必须检测垃圾对象。其次,它必须回收垃圾对象使用的堆空间并使其可供程序使用。垃圾检测通常通过定义一组根并确定根的可达性来完成。如果执行程序可以访问对象的根有一些引用路径,则该对象是可访问的。程序始终可以访问根。任何从根可到达的对象都被认为是活动的。无法访问的对象被视为垃圾,因为它们不再影响程序执行的未来过程。
在 JVM 中,根集依赖于实现,但总是在局部变量中包含任何对象引用。在 JVM 中,所有对象都驻留在堆上。局部变量驻留在 Java 堆栈上,每个执行线程都有自己的堆栈。每个局部变量要么是对象引用,要么是原始类型,例如 int、char 或 float。因此,任何 JVM 垃圾收集堆的根都将包括每个线程堆栈上的每个对象引用。根的另一个来源是加载类的常量池中的任何对象引用,例如字符串。加载类的常量池可以引用堆上存储的字符串,如类名、超类名、超接口名、字段名、字段签名、方法名、方法签名等。
根引用的任何对象都是可访问的,因此是活动对象。此外,活动对象引用的任何对象也是可访问的。该程序能够访问任何可访问的对象,因此这些对象必须保留在堆上。任何无法访问的对象都可以被垃圾回收,因为程序无法访问它们。
文章继续探索不同的垃圾收集策略,包括引用计数收集器、跟踪收集器、压缩收集器和复制收集器。
虽然这篇文章很旧,但它仍然适用于今天;并没有太大的改变。不同收集策略的性能有所改进,但没有新的重大进步。
例如,Oracle HotSpot JVM 有一个新的Garbage-First Garbage Collector,它是一个复制收集器,针对多核处理器和大堆大小进行了性能调整(有关 G1 垃圾收集器的更多信息,请参阅此答案)。
.Net 团队在 CoreCLR 开源后不久就发布了有关此主题的有趣文档: Stack Walking