参考资料:
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》第三章
- https://blog.csdn.net/eeelan/article/details/76166996
文章目录
HotSpot的垃圾回收算法实现
枚举根节点
在HotSpot的实现中,使用一组称为OopMap的数据结构来得知哪些地方存放着对象引用,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT(即时编译器)编译过程中,也会在特定的位置记录下来栈和寄存器中哪些位置是引用。这样GC在扫描的时候就可以直接得知这些信息了。
安全点
在OopMap的协助下,HotSpot可以快速准确的完成GC Root枚举,但是导致OopMap内容变化(引用关系变化)的指令非常多,如果为每一条指令生成对应的OopMap,会需要很多额外的空间。
实际上,只是在特定的位置记录了这些信息,这些位置称为安全点(程序只有在到达安全点的时候才能暂停进行GC)。安全点的选定基本上是以程序是否具有让程序长时间执行的特征为标准进行选定的——因为每条指令执行的时间都非常的短暂,程序不太可能因为指令流长度提倡这个原因而长时间运行,“长时间运行”最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生safepoint。
对于Safepoint,另一个问题,如何在Gc发生时让所有线程(不包括执行JNI调用的线程)都到最近的安全点再停顿下来。两种方法,抢先式中断和主动式中断。
- 抢先式中断:GC时,将所有线程全部中断,如果有线程不在安全点上,恢复线程,让其运行到安全点上。
- 主动式中断:GC需要中断线程的时候,简单的设置一个标志,各个线程主动去轮询这个标志,发现标志位真时就主动中断挂起,轮询标志的地方和安全点是重合的。
安全域
指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。
线程执行到安全区域中的代码时,首先标识自己进入了安全区域;在离开安全区域时,要检查系统是否已经完成了枚举根节点(或整个GC过程),完成了就继续执行,否则必须等待直到收到可以安全离开Safe Region的信号。
安全区域是为了解决线程Sleep或Blocked状态的。
垃圾收集器
新生代收集器(复制算法)
Serial收集器
单线程收集器,“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
应用场景:虚拟机运行在Client模式下的默认的新生代收集器。
原因:简单而高效,没有线程交互的开销,可以获得最高的单线程效率。用户桌面应用场景中,分配给虚拟机管理的内存(新生代)一般只有几十兆到一两百兆。客户端的停顿时间控制在几十毫秒最多一百毫秒内,只要不是频繁发生,这点停顿是可以接受的。
ParNew收集器
Serial收集器的多线程版本,使用多条线程进行垃圾收集,其余行为与Serial一样。
应用场景:Server模式下首选的新生代收集器,一个与性能无关但是重要的原因是,除了Serial,只有它能与CMS收集器配合工作。
Parallel Scavenge收集器(吞吐量优先)
新生代收集器,使用复制算法来进行收集,并行的多线程收集器,与ParNew收集器类似,但是它的特点是比较关注吞吐量。
- 吞吐量 = 运行用户代码时候 / (运行用户代码事件 + 垃圾收集时间)
- 特点:Parallel Scavenge收集器的目的是达到一个可控制的吞吐量。吞吐量即CPU用户运行用户代码的时间与CPU总消耗时间的比值。
- 应用场景:适合在后台运算而不需要交互太多的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,
-XX:MaxGCPauseMillis
参数控制最大垃圾收集停顿时间-XX:GCTimeRatio
参数直接设置吞吐量大小。
参数-XX:+UseAdaptiveSizePolicy是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小、Eden和Survivor的比例、晋升老年代对象的大小等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态地调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。
GC自适应的调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
老年代收集器。
Serial Old收集器(标记–整理算法)
Serial收集器的老年代版本,同样是一个单线程收集器
应用场景:Client模式下的虚拟机。
如果使用在Server模式下,那它主要还有两大用途途:
1.JDk1.5及之前的版本中与Parallel Scavenge收集器搭配使用,
2.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel old收集器(标记–整理算法)
Parallel Scavenge收集器的老年代版本,多线程
该收集器在JDK1.6中才开始提供
CMS收集器(标记–清除算法)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的JAVA应用集中在互联网网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望停顿越短越好,以给用户带来较好的用户体验。CMS收集器就非常符合这类的应用需求。
CMS(concurrent Mark Sweep)是基于“标记–清除”算法实现的,它的运作过程分为四个步骤:
1.初始标记: 仅仅标记一下GC Roots能直接关联到的对象,速度很快
2.并发标记: 进行GC RootsTracing的过程
3.重新标记: 为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。此阶段的停顿时间比初始标记阶段长比并发标记短。
4.并发清除
**初始标记和重新标记仍然需要“Stop The World”,**但是并发标记和并发清除过程可以和用户线程一起工作,总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS缺点:
1.CMS收集器对CPU资源非常敏感
2.CMS收集器无法处理浮动垃圾,可能出现cconcurrent Mode Failure而导致另一次Full GC的产生
3.产生大量的空间碎片
G1 收集器
G1(Garbage First):是一款面向服务端应用的垃圾收集器,用于替换CMS收集器。在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
G1将新生代,老年代的物理空间划分取消了。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
G1的特点:
1.并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的事件,G1收集器可以通过并发的方式让Java程序继续执行
2.分代收集:分代概念,虽然G1不需要其他收集器的配合就能独立管理整个Java堆,但是可以采用不同的方式去处理不同的对象。
3.空间整合:G1整体看来是基于标记整理的算法,从局部看来确实基于复制的算法实现。
4.可预测的停顿
G1之前的收集器进行收集的范围都是整个新生代或者老年代,而使用G1收集器时Java堆的内存布局就发送了变化,他将整个java堆划分为多个大小相等的独立区域(Region)。新生代和老年代是一部分Region的集合。
G1收集器的步骤:
1.初始标记
2.并发标记
3.最终标记
4.筛选回收
两个概念:
- MinorGC,次收集:在新生代发生的垃圾收集,速度快,发生频繁。
- FullGC,MajorGC,主收集:发生在年老代的垃圾收集。一般伴随一次MinorGC。
一般的规则:
- 对象优先在Eden分配。当Eden没有足够的空间时,虚拟机将发起一次MinorGC。
- 大对象直接进入老年代。大对象是指需要连续内存空间的Java对象。目的是避免在Eden区及两个Survivor区之间大量的内存复制(新生代采用复制算法收集内存)。
- 长期存活对象将进入老年代。虚拟机给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能够被Survivor容纳,将被移动到Survivor空间,并且对象年龄设为1。对象在Survivor区中每“熬过”一次MinorGC,年龄就加1,达到某个阀值就晋升到年老代。
- 空间分配担保。在发生Minor GC之前,虚拟机会先检查年老代最大可用的连续空间算法大于新生代所有对象总空间,如果是,那么Minor GC可以确保是安全的。如果否,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,这是有风险的(存活对象占用的内存大于平均大小,将导致HandlePromotionFailure失败,重新发起一次Full GC);如果小于或者HandlePromotionFailure设置不允许冒险,将改为Full GC。