概述
简单来说,垃圾收集由两步构成:查找不再使用的对象,以及释放这些对象所管理的内存。 JVM 从查找不再使用的对象(垃圾对象)入手。有时,这也被称为查找不再有任何对象引用的对象(暗指采用“引用计数”的方式统计对象引用)。
然而,这种靠引用计数的方式不太靠谱:假设有一个对象链接列表,列表中的每一个对象(除了头节点)都指向列表中的另一个对象,但是,如果没有任何一个引用指向列表头,这个列表就没人使用,可以被垃圾回收器回收。如果这是一个循环列表(即列表的尾元素反过来又指向了头元素),那么列表中的每一个元素都包含一个引用,即使这个列表内没有任何一个对象实际被使
用,因为没有任何一个对象指向这个列表。
所以引用是无法通过计数的方式动态跟踪的; JVM 必须定期地扫描堆来查找不再使用的对象。一旦发现垃圾对象, JVM 会回收这些对象所持有的内存,把它们分配给需要内存的其他对象。然而,简单地记录空闲内存也无法保证将来有足够的可用内存,有些时候,我们还必须进行内存整理来防止内存碎片。
假设以下场景,一个程序需要分配大小为 1000 字节的数组,紧接着又分配一个大小为 24字节的数组,并在一个循环中持续进行这样的分配。最终程序会耗尽整个堆,结果如图第一行所示:堆空间被占满,分配的数组间隔地分布于整个堆内。
分代
根据情况将堆划分成不同的代(Generation)。
这些代被称为“老年代”(Old Generation 或 Tenured Generation)和“新生代”(Young Generation)。
新生代又被进一步地划分为不同的区段,分别称为 Eden 空间和 Survivor 空间(不过 Eden 有时会被错误地用于指代整个新生代)。
设计的初衷是快速回收数量多而生命周期短的临时对象
分代垃圾收集器
新生代是堆的一部分,对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间。
不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方。这种操作被称为 Minor GC
优势:
其一,由于新生代仅是堆的一部分,与处理整个堆相比,处理新生代的速度更快。而这意味着应用线程停顿的时间会更短
其二:第二个优势源于新生代中对象分配的方式。对象分配于 Eden 空间(占据了新生代空间的绝大多数)。垃圾收集时,新生代空间被清空,Eden 空间中的对象要么被移走,要么被回收;所有的存活对象要么被移动到另一个 Survivor 空间,要么被移动到老年代。
由于所有的对象都被移走,相当于新生代空间在垃圾收集时自动地进行了一次压缩整理。
GC
所有的垃圾收集算法在对新生代进行垃圾回收时都存在“时空停顿”现象。对象不断地被移动到老年代,最终老年代也会被填满, JVM 需要找出老年代中不再使用的对象,并对它们进行回收。而这便是垃圾收集算法差异最大的地方。简单的垃圾收集算法直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理。这个过程被称为 Full GC,通常导致应用程序线程长时间的停顿。
另一方面,通过更复杂的计算,我们还有可能在应用线程运行的同时找出不再使用的对象; CMS 和 G1 收集器就是通过这种方式进行垃圾收集的。由于它们不需要停止应用线程就能找出不再用的对象, CMS 和 G1 收集器被称为 Concurrent 垃圾收集器。同时,由于它们将停止应用程序的可能降到了最小,也被称为低停顿(Low-Pause)收集器(有时也称为无停顿收集器,虽然这个叫法相当不确切)。
Concurrent 收集器也使用各种不同的方法对老年代空间进行压缩。使用 CMS 或 G1 收集器时,应用程序经历的停顿会更少(也更短)。
其代价是应用程序会消耗更多的 CPU。 CMS 和 G1 收集也可能遭遇长时间的 Full GC 停顿(尽量避免发生那样的停顿是这些调优算法要考虑的重要方面)。
总结就是:1. 所有的 GC 算法都将堆划分成了老年代和新生代。
2. 所有的 GC 算法在清理新生代对象时,都使用了“时空停顿”(stop-theworld)方式的垃圾收集方法,通常这是一个能较快完成的操作
GC算法(垃圾收集算法)
4种GC算法
-
Serial垃圾收集器
Serial 垃圾收集器是四种垃圾收集器中最简单的一种。如果应用运行在 Client 型虚拟机(Windows 平台上的 32 位 JVM 或者是运行在单处理器机器上的 JVM)上,这也是默认的垃圾收集器。 Serial 收集器使用单线程清理堆的内容。使用 Serial 收集器,无论是进行 Minor GC 还是Full GC,清理堆空间时,所有的应用线程都会被暂停。进行 Full GC 时,它还会对老年代空间的对象进行压缩整理。 通过 -XX:+UseSerialGC 标志可以启用 Serial 收集器(大多数情况下,如果可以使用这个标志,默认就会开启)。注意,跟大多数的 JVM 标志不同,关闭Serial 收集器不能简单地将加号符变成减号符(譬如,使用 -XX:-UseSerialGC)。 在 Serial收集器作为默认收集器的系统上,如果需要关闭 Serial 收集器,可以通过指定另一种垃圾收集器来实现。
-
Throughput垃圾收集器
Throughput 收集器是 Server 级虚拟机(多 CPU 的 Unix 机器以及任何 64 位虚拟机)的默认收集器。 Throughput 收集器使用多线程回收新生代空间, Minor GC 的速度比使用 Serial 收集器快得多。处理老年代时 Throughput 收集器也能使用多线程方式。这已经是 JDK 7u4 及之后的版本的默认行为,对于之前老版本的 JDK 7 虚拟机,通过 -XX:+UseParallelOldGC 标志可以开启这个功能。 由于 Throughput 收集器使用多线程, Throughput 收集器也常常被 称 为 Parallel 收 集 器。 Throughput 收 集 器 在 Minor GC 和 Full GC 时 会 暂 停 所 有 的 应用线程,同时在 Full GC 过程中会对老年代空间进行压缩整理。 由于在大多数适用的场景,它已经是默认的收集器,所以你基本上不需要显式地启用它。如果需要,可以使用-XX:+UseParallelGC、 -XX:+UseParallelOldGC 标志启用 Throughput 收集器
-
CMS收集器
CMS 收集器设计的初衷是为了消除 Throughput 收集器和 Serial 收集器 Full GC 周期中的长时间停顿。 CMS 收集器在 Minor GC 时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。 然而,这其中最显著的不同是, CMS 不再使用 Throughput 的收集算法(-XX:+UseParallelGC),改用新的算法来收集新生代对象(使用 -XX:+UseParNewGC 标志)。 CMS 收集器在 Full GC 时不再暂停应用线程,而是使用若干个后台线程定期地对老年代空间进行扫描,及时回收其中不再使用的对象。这种算法帮助 CMS 成为一个低延迟的收集器:应用线程只在 Minor GC 以及后台线程扫描老年代时发生极其短暂的停顿。应用程序线程停顿的总时长与使用 Throughput 收集器比起来短得多。 这里额外付出的代价是更高的 CPU 使用:必须有足够的 CPU 资源用于运行后台的垃圾收集线程,在应用程序线程运行的同时扫描堆的使用情况。除此之外,后台线程不再进行任何压缩整理的工作,这意味着堆会逐渐变得碎片化。 如果 CMS 的后台线程无法获得完成他们任务所需的 CPU 资源,或者如果堆变得过度碎片化以至于无法找到连续空间分配对象, CMS 就蜕化到 Serial 收集器的行为:暂停所有应用线程,使用单线程回收、整理老年代空间。这之后又恢复到并发运行,再次启动后台线程(直到下一次堆变得过度碎片化)。 通过 -XX:+UseConcMarkSweepGC、 -XX:+UseParNewGC 标志(默认情况下,这两个标志都是禁用的)可以启用 CMS 垃圾收集器。
-
G1垃圾收集器
G1 垃圾收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于 4GB)时产生的停顿。 G1 收集算法将堆划分为若干个区域(Region),不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然采用暂停所有应用线程的方式,将存活对象移动到老年代或者 Survivor 空间。同其他的收集算法一样,这些操作也利用多线程的方式完成。 G1 收集器属于 Concurrent 收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。由于老年代被划分到不同的区域, G1 收集器通过将对象从一个区域复制到另一个区域,完成对象的清理工作,这也意味着在正常的处理过程中, G1 收集器实现了堆的压缩整理(至少是部分的整理)。因此,使用 G1 收集器的堆不大容易发生碎片化——虽然这种问题无法避免。 同 CMS 收集器一样,避免 Full GC 的代价是消耗额外的 CPU 周期:负责垃圾收集的多个后台线程必须能在应用线程运行的同时获得足够的 CPU 运行周期。通过标志 -XX:+UseG1GC默认值是关闭的)可以启动 G1 垃圾收集器。
总结就是:
- 这四种垃圾收集算法分别采用了的不同的方法来缓解 GC 对应用程序的影响。
- Serial 收集器常用于仅有单 CPU 可用以及当其他程序会干扰 GC 的情况(通常是默认值)。
- Throughput 收集器在其他的虚拟机上是默认值,它能最大化应用程序的总吞吐量,但是有些操作可能遭遇较长的停顿。
- CMS 收集器能够在应用线程运行的同时并行地对老年代的垃圾进行收集。如果 CPU 的计算能力足以支撑后台垃圾收集线程的运行,该算法能避免应用程序发生 Full GC。
- G1 收集器也能在应用线程运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生 Full GC 的风险。 G1 的设计理念使得它比
CMS 更不容易遭遇 Full GC。
《JAVA性能权威指南》