Java虚拟机JVM性能优化(三):垃圾收集详解
Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源。在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制。她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的)。
垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java虚拟机动态内存管理机制的核心部分。在一个典型的垃圾收集周期里,所有仍然被引用的对象(因此是可达的)都将被保留,而那些不再被引用的对象将被释放、其所占用的空间将被回收用来分配给新的对象。
为了理解垃圾收集机制和各种垃圾收集算法,首先需要知道关于Java平台内存模型的一些知识。
垃圾收集和Java平台内存模型
当用命令行启动一个Java程序并指定启动参数-Xmx时(例如:java -Xmx:2g MyApp),指定大小的内存就分配给了Java进程,这就是所谓的Java堆。这个专用的内存地址空间用于存储Java程序(有时是JVM)所创建的对象。随着应用程序运行并不断为新对象分配内存,Java堆(即专门的内存地址空间)就会慢慢被填满。
最终Java堆会被填满,也就是说内存分配线程找不到一块足够大的连续空间为新对象分配内存,这时JVM决定要通知垃圾收集器并启动垃圾收集。垃圾收集也可以通过在程序中调用System.gc()来触发,但使用System.gc()并不能确保垃圾收集一定被执行。在任何一次垃圾收集之前,垃圾收集机制都会首先判断执行垃圾收集是否安全,当应用程序的所有活动线程都处于安全点时就可以开始执行一次垃圾收集。例如:当正在为对象分配内存时就不能执行垃圾收集,或者是正在优化CPU指令时也不能执行垃圾收集,因为这样很可能会丢失上下文从而搞错最终结果。
垃圾收集器不能回收任何一个有活动引用的对象,那将破坏Java虚拟机规范。也无需立即回收死对象,因为死对象最终还是会被后续的垃圾收集所回收。尽管有很多种垃圾收集的实现方法,但以上两点对所有垃圾收集实现都是相同的。垃圾收集真正的挑战在于如何识别对象是否存活以及如何在尽量不影响应用程序的情况下回收内存,因此垃圾收集器的目标有以下两个:
1.迅速释放没有引用的内存以满足应用程序的内存分配需要从而避免内存溢出。
2.回收内存时对正在运行的应用程序性能(延迟和吞吐量)的影响最小化。
两类垃圾收集
在本系列的第一篇中,我介绍了两种垃圾收集的方法,即引用计数和跟踪收集。接下来我们进一步探讨这两种方法,并介绍一些在生产环境中使用的跟踪收集算法。
引用计数收集器
引用计数收集器记录了指向每个Java对象的引用数,一旦指向某个对象的引用数为0,那么就可以立即回收该对象。这种即时性是引用计数收集器的主要优点,而且维护那些没有引用指向的内存几乎没有开销,不过为每个对象记录最新的引用数却是代价高昂的。
引用计数收集器的主要难点在于如何保证引用计数的准确性,另外一个众所周知的难点是如何处理循环引用的情况。如果两个对象彼此引用,而且没有被其他活动对象所引用,那么这两个对象的内存永远都不会被回收,因为指向这两个对象的引用数都不为0。对循环引用结构的内存回收需要major analysis(译者注:Java堆上的全局分析),这将增加算法的复杂性,从而也给应用程序带来额外的开销。
跟踪收集器
跟踪收集器基于这样的假设:所有的活动对象都可以通过一个已知的初始活动对象集合的迭代引用(引用以及引用的引用)找到。可以通过分析寄存器、全局对象和栈帧来确定初始活动对象集合(也被称为根对象)。确定了初始对象集合后,跟踪收集器顺着这些对象的引用关系依次将引用所指向的对象标注为活动对象,就这样已知的活动对象集合不断扩大。这一过程持续进行直到所有被引用的对象都被标注为活动对象,而那些没有被标注过的对象的内存就被回收。
跟踪收集器不同于引用计数收集器主要在于它可以处理循环引用结构。多数的跟踪收集器都是在标记阶段发现那些循环引用结构中的无引用对象。
跟踪收集器是动态语言中最常用的内存管理方式,也是目前Java中最常见的方式,同时在生产环境中也被验证了很多年。下面我将从实现跟踪收集的一些算法开始介绍跟踪收集器。
跟踪收集算法
复制垃圾收集器和标记-清除垃圾收集器并不是什么新东西,但它们仍然是目前实现跟踪收集的两种最常见算法。
复制垃圾收集器
- 上一篇:浅析java创建文件和目录
- 下一篇:Java虚拟机JVM性能优化(二):编译器