Java语言基础

3、垃圾收集与内存分配策略(2)(JVM笔记)

2017-04-06  本文已影响45人  yjaal

四、HotSpot的算法实现

4.1 枚举根节点

从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局的引用(例如常量或类静态属性)与执行上下文(如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项工作必须在一个能确保一致性的快照中进行——这里“一致性”的意思是指整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因,机试在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时页式必须停顿的。

由于目前的主流Java虚拟机使用的都是准确式GC(虚拟机可以知道内存中某个位置的数据具体是什么类型),所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

4.2 安全点

OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。

实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短,所以程序不太可能因为指令流太长而长时间运行,能让程序长时间运行最明显的特征就是指令序列复用,如方法调用、循环跳转等,所以具有这些功能的指令才会产生安全点。

对于安全点,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension):

4.3 安全区域

在程序执行时,程序在不太长的时间内就可能遇到可进入GC的安全点。但是当程序“不执行”的时候呢?所谓程序不执行就是没有分配CPU时间,典型的例子就是处于睡眠或Blocked状态,这种时候程序无法响应JVM的中断请求。对于这种情况居需要安全区域来解决。

安全区域就是指在一段代码中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,可以将Safe Region看作被扩展的Safepoint。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。当线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程继续执行,否则它就必须等待直到可以安全离开Safe Region的信号为止。

五、垃圾收集器

Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别。这里讨论的收集器基于JDK1.7 Update 14之后的HotSpot虚拟机(这个版本中正是提供了商用的G1收集器,之前G1处于实验状态)。这个虚拟机所包含的收集器如图所示。

1
图中如果两个收集器之间存在连线,就说明它们可以搭配使用。

5.1 Serial 收集器

此收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,但“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。下图示意了Serial/Serial Old收集器的运行过程。

2
JDK 1.3开始,一直到新的JDK 1.7HotSpot虚拟机开发团队为消除或者减少工作线程因内存回收而导致停顿的努力一直在进行着,开发了很多优秀的收集器,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除(这里暂不包括RTSJ中的收集器)。

虽然此收集器有缺点,但是到现在为止,依然是虚拟机运行在Client模式下的默认新生代收集器。有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。对于运行在Client模式下的虚拟机来说是一个很好的选择。

5.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本(也是一个新生代收集器),除了使用多条线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数(如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,共用了相当多的代码。工作过程如下:

3

此收集器除了多线程之外,与Serial收集器相比并没有太多创新之处,但是它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为目前只有(除Serial之外)它能与CMS收集器(是HotSpot中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作)配合工作。

ParNew收集器也是使用-XX:UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:UseParNewGC选项来强制指定。此收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

注意:

5.3 Parallel Scavenge收集器

此收集器是一个新生代收集器,它也是使用复制算法的收集器,看上去和ParNew收集器一样,但是有什么不同?

此收集器的特定是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了一百分钟,其中垃圾收集花掉一分钟,那吞吐量就是99%

停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量可以高效率利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于零的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。但是不是说这个值设置的越小,垃圾收集的速度就越快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间换取的:系统把新生代调小一些,收集300M新生代肯定比收集500M快,这也导致垃圾收集发生的更频繁,原来十秒收集一次、每次停顿一百毫秒,现在变成五秒收集一次、每次停顿七十秒,停顿时间的确下降了,但是吞吐量也下降了。

GCTimeRatio参数的值应当是一个大于零小于一百的整数,相当于吞吐量的倒数。如果此参数值为19,那允许垃圾收集的时间就占总时间的5%(即1/(1+19)),默认值是99

这个收集器是一个“吞吐量优先”的收集器,除了上述两个参数之外,还有一个参数-XX:UseAdaptiveSizePolicy值得关注。这是一个开关参数。当这个参数打开之后,就不需要手工指定其他参数等细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最何时的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。这是一个不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,具体细节就不用管了。

5.4 Serial Old收集器

这个收集器Serial收集器的一个老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。其主要意义是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge搭配使用;另一用途就是作为CMS收集器的后台预案,在并发收集发生oncurrent Mode Failure时使用。其运行过程和Serial收集器类似。

5.5 Parallel Old收集器

此收集器Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在JDK 1.6之前,如果新生代选择了Parallel Scavenge收集器,则老年代必须选择Serial Old收集器(Parallel Scavenge无法与CMS配合使用)。由于Serial Old的拖累,这两者的组合在单线程的效率可能比不上ParNewCMS的组合。这个收集器出现之后,“吞吐量优先”收集器终于有了名副其实的应用组合。其运行过程和Parallel Scavenge类似。

5.6 CMS收集器

此收集器是一种以获得最短回收停顿时间为目标的收集器。大量用在B/S系统的服务器上,因为这类应用注重响应速度,给用户较好的体验。

此收集器是基于“标记-清除”算法实现的,运行过程较为复杂,分为四个步骤:

由于整个过程中耗时最长的并发标记和并发清除过程,收集器都可以与用于线程一起工作,所以,总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。运行过程如下:

4

CMS还远达不到完美的程度,它有一下三个明显的缺点:

上一篇下一篇

猜你喜欢

热点阅读