
Java 垃圾回收机制
垃圾回收(Garbage Collection,简称 GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行。
垃圾回收算法
标记-清除
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个的垃圾收集动作。
复制算法
为了解决效率问题,一种称为复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上,然后再把已经使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价是内存缩小为原来的一半。
标记-整理算法
复制收集算法在对象成活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以,老年代一般不能直接选用这种算法。
分代收集算法
根据对象存活周期的不同将内存划分为几块。一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,每次垃圾收集时都发现大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率较高,没有额外的空间对它进行分配担保,就必须使用标记-清理
和标记-整理
算法来进行回收。
垃圾收集器
Serial 收集器
标记-复制。
单线程,一个 CPU 或一条收集线程去完成垃圾收集工作,收集时必须暂停其他所有的工作线程,直到它结束。
虽然如此,它依然是虚拟机运行在 Client 模式下的默认新生代收集器。简单而高效。
ParNew 收集器
ParNew是Serial收集器的多线程版本。Server模式下默认新生代收集器,除了Serial收集器之外,只有它能与CMS 收集器
配合工作。
Parallel Scavenge 收集器
Parallel Scavenge 收集器
是一个新生代收集器,它也是使用复制算法的收集器。特点是它的关注点与其他收集器不同,CMS等收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间和CPU总小号时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间),虚拟机总共运行了100min,其中垃圾收集花费了1min,那吞吐量就是99%.
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,主要适合在后台运算而不需要太多交互的任务。
它提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis
以及直接设置吞吐量大小的 -XX:GCTimeRatio
。
Serial Old收集器
是 Seria l收集器
的老年代版本,它同样是一个单线程收集器。给Client
模式下的虚拟机使用。
新生代采用复制算法
,暂停所有用户线程,老年代采用标记-整理算法
,暂停所有用户线程。
Parallel Old收集器
是 Parallel Scavenge 收集器
的老年代版本。这是”吞吐量优先“,注重吞吐量以及CPU资源敏感的场合都可以优先考虑Parallel Scavenge
和 Parallel Old
。Java8 默认就是这个。
CMS 收集器
CMS(Concurrent Mark Sweep) 收集器
是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类尤其重视服务的响应速度,希望系统停顿时间最短。CMS 收集器
就非常符合这类应用的需求。
CMS基于 标记-清除算法
实现。整个过程分为4个步骤:
- 初始标记(CMS initial mark) -stop the world
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark) -stop the world
- 并发清除(CMS concurrent sweep)
重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段停顿比初始标记稍微长,但远比并发标记的时间短。
整个过程耗时最长的并发标记和并发清除过程,收集器都可以与用户线程一起工作。总体上来说,CMS 收集器的内存回收过程与用户线程一起并发执行的。
特点: 并发收集,低停顿。
缺点:
- 对CPU资源非常敏感。默认启动的回收线程数是(CPU + 3) / 4. 当 CPU 4 个以上时,并发回收垃圾收集线程不少于 25% 的 CPU 资源。
- 无法处理浮动垃圾(Floating Garbage), 可能出现
”Concurrent Mode Failure“
失败而导致另一次 Full GC 的产生。由于 CMS 并发清理时,用户线程还在运行,伴随产生新垃圾,而这一部分出现在标记之后,只能下次 GC 时再清理。这一部分垃圾就称为”浮动垃圾“。
由于 CMS 运行时还需要给用户空间继续运行,则不能等老年代几乎被填满再进行收集,需要预留一部分空间提供并发收集时,用户程序运行。JDK1.6 中,CMS 启动阈值为 92%. 若预留内存不够用户使用,则出现一次Concurent Mode Failure
失败。这时虚拟机启动后备预案,临时启用Serial Old
收集老年代,这样停顿时间很长。 - CMS 基于
标记-清除算法
实现,则会产生大量空间碎片,空间碎片过多时,没有连续空间分配给大对象,不得不提前触发一次FUll GC
。当然可以开启-XX:+UseCMSCompactAtFullCollection
(默认开),在 CMS 顶不住要 FullGC 时开启内存碎片合并整理过程。内存整理过程是无法并发的,空间碎片问题没了,但停顿时间变长。
G1
G1将新生代,老年代的物理空间划分取消了。这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法
将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor 空间。老年代也分成很多区域,G1 收集器
通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1 完成了堆的压缩(至少是部分堆的压缩),这样也就不会有 CMS 内存碎片问题的存在了。
在 G1 中,还有一种特殊的区域,叫 Humongous 区域。 如果一个对象占用的空间超过了分区容量 50% 以上,G1收集器
就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous区,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1 Young GC
主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时会被触发。在这种情况下,Eden 空间的数据移动到Survivor 空间中,如果Survivor 空间不够,Eden 空间的部分数据会直接晋升到年老代空间。Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终 Eden 空间的数据为空,GC 停止工作,应用线程继续执行。
G1 Mix GC
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
- 全局并发标记(global concurrent marking)
- 拷贝存活对象(evacuation)