最糟糕的是人们在生活中经常受到错误志向的阻碍而不自知,真到摆脱了那些阻碍时才能明白过来。 —— 歌德
1. 对象的标记过程
1.1 快速枚举GC Roots
在可达性分析过程中,为了准确找出与GC Roots相关联的对象,必须要求整个执行系统看起来像是被冻结在某个时间点上,即暂停所有运行中的线程,不可以出现对象的引用关系还在不断变化的情况,这点是导致GC时必须停顿所有线程(“Stop the world”)的一个重要原因。
可作为GC Roots的节点主要在全局性的引用(例如常量或静态类属性)与执行上下文(本地变量表中的引用)中。
在HosSpots中,是使用一组称为OopMap的数据结构来存放着对象的引用。类加载完成时,HotSpot把对象内什么偏移量上是什么类型的数据计算出来存储到OopMap中,通过JIT编译出来的本地代码,也会记录下栈和寄存器中哪些位置是引用。通过扫描OopMap的数据就可以快速标识出存活的对象。
1.2 安全的进行GC
在发生GC时,JVM通过安全点和安全区域来保证GC可以安全的进行。
1.2.1 安全点
线程运行时,只有在到达安全点(Safe Point)才能停顿下来进行GC。
基于OopMap数据结构,HotSpot可以快速完成GC Roots的遍历,不过HotSpot并不会为每条指令都生成对应的OopMap,只会在Safe Point处记录这些信息。
当发生GC时,不直接对线程进行中断操作,而是简单的设置一个中断标志,每个线程运行到Safe Point的时候,主动去轮询这个中断标志,如果中断标志为真,则将自己进行中断挂起。
1.2.2 安全区域
处于Sleep或Blocked状态的线程在GC时无法响应JVM的中断请求,无法到Safe Point去中断挂起,对于这种情况,可以使用安全区域(Safe Region)来解决。
Safe Region是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
- 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程。
- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止。
2. 垃圾收集器
垃圾收集器是内存回收的具体实现,JAVA虚拟机没有规定垃圾收集器该如何实现,且提供参数供用户根据自己的要求将垃圾收集器进行组合使用。
上图展示了7种不同的分代收集器,若两个收集器直接存在连线,则说明可以组合使用。虚拟机所处的区域,则表示是属于新生代收集器还是老年代收集器。
2.1 Serial收集器(新生代串行收集器)
串行收集器主要有两个特点:第一,仅使用单线程进行垃圾回收;第二,它是独占式的垃圾回收。
之所以是独占式的,是因为在串行收集器运行时,应用程序的所有线程都停止工作,进行等待。
虽然如此,串行收集器却是一个极为高效的收集器,使用的是复制算法,实现相对简单,且没有线程的切换开销。
使用-XX:+UseSerialGC参数可以指定使用新生代串行收集器和老年代串行收集器。当JVM在Client模式下,Serial收集器是默认的垃圾收集器。
2.2 ParNew收集器(并行收集器)
并行收集器工作在新生代,只是简单的将串行收集器多线程化,回收策略、算法以及参数和串行收集器是一样的。并行收集器也是独占式的,在GC过程中,应用程序也会全部暂停,由于使用多线程进行垃圾回收,因此产生的停顿时间要短于串行收集器。
开启并行收集器可以使用以下参数:
(1) -XX:UseParNewGC: 新生代使用并行收集器,老年代使用串行收集器。
(2) -XX:UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用CMS。
并行收集器GC时的线程数量可以使用-XX:ParallelGCThreads参数指定。
2.3 新生代并行回收(Parallel Scavenge)收集器
Parallel Scavenge收集器是一个采用多线程基于复制算法并工作在新生代的收集器,其中一个特点是它比较关注系统的吞吐量。
吞吐量 = 用户代码运行时间 /(用户代码运行时间 + 垃圾收集时间)
使用以下参数可以开启Parallel Scavenge:
(1) -XX:+UseParallelGC: 新生代使用Parallel Scavenge,老年代使用串行收集器。
(2) -XX:+UseParallelOldGC: 新生代和老年代都使用并行回收收集器。
Parallel Scavenge提供了两个参数用于精确控制吞吐量:
1、-XX:MaxGCPauseMillis 设置垃圾收集的最大停顿时间。
2、-XX:GCTimeRatio 设置吞吐量大小。
除此之外,Parallel Scavenge还支持自适应的GC调节策略,使用-XX:UseAdaptiveSizePolicy可以打开自适应的GC调节策略。在这种模式下,新生代的大小,eden和survivor的比例,晋升老年代的对象年龄等参数会被自动调整,以达到系统运行的最优化。
2.4 Serial Old收集器
Serial Old 是一个采用单线程基于标记-整理算法并工作在老年代的收集器,是Client模式下老年代默认的收集器。
2.5 Parallel Old收集器(并行GC)
Parallel Old是一个采用多线程基于标记-整理算法并工作在老年代的收集器。同样也是一个关注系统吞吐量的收集器。
2.6 CMS收集器
CMS主要关注系统的停顿时间,使用的是标记-清除算法,工作在老年代,GC时,分为以下四个阶段:
(1) 初始标记:这个过程只是标记需要回收的对象,这个阶段仍然会Stop The World。
(2) 并发标记:进行GC Roots Tracing的过程,可以和用户线程一起工作。
(3) 重新标记:用于修正并发标记期间由于用户程序继续运行而导致标记产生变动的那部分记录,这个过程也会Stop The World。
(4) 并发清除:多个线程并行进行垃圾回收,可以和用户线程一起工作。
CMS收集器的缺点:
(1) 对CPU资源比较敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程资源,降低系统的总吞吐量。
(2) 无法处理浮动垃圾,在并发清理阶段,用户线程的运行依然会产生新的垃圾对象,这部分垃圾只能在下一次GC时收集。
(3) CMS是基于标记-清除算法实现的,意味着收集结束后会造成大量的内存碎片,可能导致出现老年代剩余空间很大,却无法找到足够大的连续空间分配当前对象,不得不提前触发一次Full GC。
2.7 G1收集器
G1(Garbage First)是JDK1.7提供的一个工作在新生代和老年代的收集器,基于“标记-整理”算法实现,在收集结束后可以避免内存碎片问题。
G1收集器有以下优点:
(1) 并行与并发:充分利用多CPU、多核来缩短Stop the world停顿时间。
(2) 分代收集:不需要其他收集器配合就可以管理整个Java堆,采用不同的方式处理新建的对象、已经存活一段时间和经历过多次GC的对象获取更好的收集效果。
(3) 空间整合:G1在运行期间不会产生内存空间碎片,有利于应用的长时间运行,且分配大对象时,不会导致由于无法申请到足够大的连续内存而提前触发一次Full GC。
(4) 可预测的停顿:G1中可以建立可预测的停顿时间模型,能让使用者明确指定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代不再试物理隔离了,都是一部分Region(不需要连续)的集合。G1跟踪各个Region里的垃圾堆积的大小,在后台维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region,避免在整个Java堆上进行全区域的垃圾回收,确保了G1收集器可以在有限的时间内尽可能收集更多的垃圾。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,JVM都是使用Remembered Set数据结构来存储,以避免全堆扫描。G1中每个Region都有一个对应的Remenbered Set,当虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于相同的Region中,如果不是,则通过CardTable把相关引用信息记录到被引用对象所属Region的Remenbered Set中。
G1收集器GC时,主要分为以下四个阶段:
(1) 初始标记:标记与GC Roots直接相关联的对象,该阶段会Stop the world。
(2)并发标记:从 GC Roots开始对堆中的对象进行可达性分析,找出存活对象,可与应用线程并发执行。
(3)最终标记:为了修正在并发标记期间因用户线程执行导致标记产生变化的那一部分标记记录。
(4)筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。