Java理论与实践: JVM 1.4.1中的垃圾收集

上个月,我们分析了引用计数、复制、标记-清除和标记-整理这些经典的垃 圾收集技术。其中每一种方法在特定条件下都有其优点和缺点。例如,当有很多 对象成为垃圾时,复制可以做得很好,但是有许多长寿对象时它就变得很糟(要 反复复制它们)。相反,标记-整理对于长寿对象可以做得很好(只复制一次) ,但是当有许多短寿对象时就没有那么好了。JVM 1.2 及以后版本使用的技术称 为 分代垃圾收集(generational garbage collection),它结合了这两种技术 以结合二者的长处,结果就是对象分配开销非常小。

老对象和年轻对象

在任何一个应用程序堆中,一些对象在创建后很快就成为垃圾,另一些 则在程序的整个运行期间一直保持生存。经验分析表明,对于大多数面向对象的 语言,包括 Java 语言,绝大多数对象――可以多达 98%(这取决于您对年轻对 象的衡量标准)是在年轻的时候死亡的。可以用时钟秒数、对象分配以后�h内存管理子系统分配的总字节或者对象分配后经历的垃圾收集的次数 来计算对象的寿命。但是不管您如何计量,分析表明了同一件事――大多数对象 是在年轻的时候死亡的。大多数对象在年轻时死亡这一事实对于收集器的选择很 有意义。特别是,当大多数对象在年轻时死亡时,复制收集器可以执行得相当好 ,因为复制收集器完全不访问死亡的对象,它们只是将活的对象复制到另一个堆 区域中,然后一次性收回所有的剩余空间。

那些经历过第一次垃圾收集 后仍能生存的对象,很大部分会成为长寿的或者永久的对象。根据短寿对象和长 寿对象的混合比例,不同垃圾收集策略的性能会有非常大的差别。当大多数对象 在年轻时死亡时,复制收集器可以工作得很好,因为年轻时死亡的对象永远不需 要复制。不过,复制收集器处理长寿对象却很糟糕,它要从一个半空间向另一个 半空间反复来回复制这些对象。相反,标记-整理收集器对于长寿对象可以工作 得很好,因为长寿对象趋向于沉在堆的底部,从而不用再复制。不过,标记-清 除和标记-理整收集器要做很多额外的分析死亡对象的工作,因为在清除阶段它 们必须分析堆中的每一个对象。

分代收集

分代收集器(generializational collector)将堆分为多个代。在年轻的代中 创建对象,满足某些提升标准的对象,如经历了特定次数垃圾收集的对象,将被 提升到下一更老的代。分代收集器对不同的代可以自由使用不同的收集策略,对 各代分别进行垃圾收集。

小的收集

分代收集的一个优点是它不同时收集所有的代,因此可以使垃圾收集暂停更 短。当分配器不能满足分配请求时,它首先触发一个 小的收集(minor collection),它只收集最年轻的代。因为年轻代中的许多对象已经死亡,复制 收集器完全不用分析死亡的对象,所以小的收集的暂停可以相当短并通常可以回 收大量的堆空间。如果小的收集释放了足够的堆空间,那么用户程序就可以立即 恢复。如果它不能释放足够的堆空间,那么它就继续收集上一代,直到回收了足 够的内存。(在垃圾收集器进行了全部收集以后仍不能回收足够的内存时,它将 扩展堆或者抛出 OutOfMemoryError )。

代间引用

跟踪垃圾收集器,如复制、标记-清除和标记-整理等垃圾收集器,都是从根集 (root set)开始扫描,遍历对象间的引用,直到访问了所有活的对象。

分代跟踪收集器从根集开始,但是并不遍历指向更老一代中对象的引用,这 减少了要跟踪的对象图的大小。但是这也带来一个问题――如果更老一代中的对 象引用一个不能通过从根开始的所有其他引用链到达的更年轻的对象该怎么办?

为了解决这个问题,分代收集器必须显式地跟踪从老对象到年轻对象的引用 并将这些老到年轻的引用加入到小的收集的根集中。有两种创建从老对象到年轻 对象的引用的方法。要么是将老对象中包含的引用修改为指向年轻对象,要么是 将引用其他年轻对象的年轻对象提升为更老的一代。

跟踪代间引用

不管一个老到年轻的引用是通过提升还是指针修改创建的,垃圾收集器在进 行小的收集时需要有全部老到年轻的引用。做到这一点的一种方法是跟踪老的代 ,但是这显然有很大的开销。更好的一种方法是线性扫描老的代以查找对年轻对 象的引用。这种方法比跟踪更快并有更好的区域性(locality),但是仍然有很 大的工作量。

赋值函数(mutator)和垃圾收集器可以共同工作以在创建老到年轻的引用时 维护它们的完整列表。当对象提升为更老一代时,垃圾收集器可以记录所有由于 这种提升而创建的老到年轻的引用,这样就只需要跟踪由指针修改所创建的代间 引用。

垃圾收集器可以有几种方法跟踪由于修改现有对象中的引用而产生的老到年 轻的引用。它可以使用在引用计数收集器中维护引用计数的同样方法(编译器可 以生成围绕指针赋值的附加指令)跟踪它们,也可以在老一代堆上使用虚拟内存 保护以捕获向老对象的写入。另一种可能更有效的虚拟内存方法是在老一代堆中 使用页修改脏位(page modification dirty bit),以确定为找到包含老到年 轻指针的对象时要扫描的块。

用一点小技巧,就可以避免跟踪每一个指针修改并检查它是否跨越代边界的 开销。例如,不需要跟踪针对本地或者静态变量的存储,因为它们已经是根集的 一部分了。也可以避免跟踪存储在某些构造函数中的指针,这些构造函数只用于 初始化新建对象的字段(即所谓 初始化存储(initializing stores)),因为 (几乎)所有对象都是分配到年轻代中。不管是什么情况,运行库都必须维护一 个老对象到年轻对象的引用集并在收集年轻代时将这些引用添加到根集中。

在图 1 中,箭头表示堆中对象间的引用。红色箭头表示必须添加到根集中供 小的收集使用的老到年轻的引用。蓝色箭头表示从根集或者年轻代到老对象的引 用,在只收集年轻代时不需要跟踪它们。

图 1. 代间引用

时间: 2024-11-27 08:50:57

Java理论与实践: JVM 1.4.1中的垃圾收集的相关文章

Java理论与实践专题

Java理论与实践: JDK 5.0中更灵活.更具可伸缩性的锁定机制 Java理论和实践: 一个有缺陷的微基准的剖析 Java理论和实践: 理解JTS ― 平衡安全性和性能 Java理论和实践: 理解JTS ― 幕后魔术 Java理论和实践: 安全构造技术 Java理论与实践: 平衡测试,第3部分:用方面检验设计约束 Java理论与实践:平衡测试,第2部分:编写和优化bug检测器 Java理论与实践:平衡测试,第1部分:不要仅编写测试,还要编写bu Java理论与实践: 您的小数点到哪里去了?

Java理论与实践: 垃圾收集简史

Java 语言可能是使用最广泛的依赖于垃圾收集的编程语言,但是它并不是第 一个.垃圾收集已经成为了包括 Lisp.Smalltalk.Eiffel.Haskell.ML. Scheme和 Modula-3 在内的许多编程语言的一个集成部分,并且从 20 世纪 60 年代早期就开始使用了.在 Java 理论与实践的本篇文章中,Brian Goetz 描述 了垃圾收集最常用的技术. 垃圾收集的好处是无可争辩的 ―― 可靠性提高.使内存管理与类接口设计 分离,并使开发者减少了跟踪内存管理错误的时间.著

Java理论与实践: 消除bug

很多有关编程风格的建议都是为了创建高质量.可维护的代码,这很合理, 因为最容易修复 bug 的时间就是在产生 bug 之前(少量的预防措施--).遗 憾的是,只预防往往是不够的,虽然有一些精巧的工具可以帮助您创建好的代码 ,但是很少有工具可以帮助您分析.维护或提高现有代码的质量. 写线程安全的类很难,而分析现有类的线程安全性更难,增强类使其仍然保 持线程安全也很难.以隐含假定.不变式以及预期用例(虽然在开发人员的头脑 中很清晰,但是没有以设计笔记.注释或者文档的方式记录下来)的方式编写完 类之后

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

伸缩 内容: synchronized 快速回顾 对 synchronized 的改进 比较 ReentrantLock 和 synchronized 的可伸缩性 条件变量 这不公平 结束语 参考资料 关于作者 对本文的评价 相关内容: Java 理论与实践 系列 Synchronization is not the enemy Reducing contention IBM developer kits for the Java platform (downloads) 订阅: develop

Java理论与实践: 构建一个更好的HashMap

ConcurrentHashMap 是 Doug Lea的 util.concurrent 包的一部分,它提供 比Hashtable 或者 synchronizedMap 更高程度的并发性.而且,对于大多数成 功的 get() 操作它会设法避免完全锁定,其结果就是使得并发应用程序有着非 常好的吞吐量.这个月,BrianGoetz 仔细分析了 ConcurrentHashMap的代码, 并探讨 Doug Lea 是如何在不损失线程安全的情况下取得这么骄人成绩的. 在7月份的那期 Java理论与实践

Java理论与实践: 修复Java内存模型,第1部分

活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议.原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile .final 以及 synchronized.在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM.这些更改有些已经 集成在 JDK 1.4 中:而另一些将会包含

Java 理论和实践: 了解泛型 识别和避免学习使用泛型过程中的陷阱

简介: JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进.但是,对于初次使用泛型类型的用户来说,泛型的某些方面看起来可能不容易明白,甚至非常奇怪.在本月的"Java 理论和实践"中,Brian Goetz 分析了束缚第一次使用泛型的用户的常见陷阱.您可以通过 讨论论坛与作者和其他读者分享您对本文的看法.(也可以单击本文顶端或底端的 讨论来访问这个论坛.) 表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型)都类似于 C++ 中的模板.但是

Java 理论与实践: 非阻塞算法简介

[本文转载自Java 理论与实践: 非阻塞算法简介]Java 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能.非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御.在这期的 Java 理论与实践 中,并发性大师 Brian Goet

Java 理论与实践:变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用.尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变.在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则.请在附带的论坛中与作者和其他读者分享您关于本文的心得.(也可以单击文章顶部或底部的"讨论"来访问论坛.) 不变对象是指在实例化后其外部可见状态无法更改的对象.Java 类库中的 String.