经典的垃圾收集器共有七款:
新生代垃圾收集器:Serial、ParNew、Parallel Scavenge
老年代垃圾收集器:CMS、Serial Old、Parallel Old
全堆垃圾收集器:G1
如果两个垃圾收集器之间存在连线,说明它们可以搭配着使用。
serial收集器是最基础、历史最悠久的收集器,是新生代 收集器中的一种,是基于标记-复制算法实现的收集器,同时也是一个单线程工作的收集器,在进行垃圾收集的时候会停止其他线程工作,产生Stop The World。
尽管它会致使用户线程长时间停顿,但是它简单高效
Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择
ParNew是Serial收集器的多线程并行版本,是基于标记-复制算法实现的收集器,也是新生代的垃圾收集器,JDK8中默认的新生代垃圾收集器,JDK9就不是了
在单核环境下,ParNew收集器并不比Serial收集器效果好,但是当处理器核心数量较多时,ParNew还是可以高效地进行垃圾收集,默认的收集线程数与处理器核心数相同。
使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数
Parallel Scavenge收集器也是一款新生代收集器, 是基于标记-复制算法实现的收集器。它的目标是达到一个可控制的吞吐量,因此也被叫做吞吐量优先收集器
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量
控制最大垃圾收集停顿时间的-XX: MaxGCPauseMillis参数
参数是一个大于0的毫秒数,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的,停顿时间越少,垃圾收集的频率更快,原来10秒收集一次、 每次停顿100毫秒, 现在变成5秒收集一次、 每次停顿70毫秒。 停顿时间的确在下降, 但吞吐量也降下来了。
设置吞吐量大小的-XX: GCTimeRatio参数
参数是一个大于0小于100的整数 ,是垃圾收集占总时间的比率。
此外,Parallel Scavenge还拥有一个参数-XX: +UseAdaptiveSizePolicy ,这是一个开关参数, 当这个参数被激活之后, 就不需要人工指定新生代的大小(-Xmn) 、 Eden与Survivor区的比例(-XX: SurvivorRatio) 、 晋升老年代对象大小(-XX: PretenureSizeThreshold) 等细节参数了, 虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 这种调节方式称为垃圾收集的自适应的调节策略 。只需要把基本的内存数据设置好(如-Xmx设置最大堆) , 然后使用-XX: MaxGCPauseMillis参数(更关注最大停顿时间) 或-XX: GCTimeRatio(更关注吞吐量) 参数给虚拟机设立一个优化目标, 那具体细节参数的调节工作就由虚拟机完成了。 自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。在服务端只有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure时使用。
Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法实现。 在注重 吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。 是基于标记-清除算法实现的, 它的运作 过程相对于前面几种收集器来说要更复杂一些, 整个过程分为四个步骤
由于在整个过程中耗时最长的并发标记和并发清除阶段中, 垃圾收集器线程都可以与用户线程一起工作, 所以从总体上来说, CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的三个缺点:
G1是一款主要面向服务端应用的垃圾收集器,是一款基于停顿时间模型的垃圾收集器。JDK 9发布之日, G1宣告取代Parallel Scavenge加Parallel Old组合, 成为服务端模式下的默认垃圾收集器。
以往的垃圾收集器都是基于分代思想设计的,垃圾收集在特定的分代区域内。G1使用基于Region的堆内存布局的垃圾收集器,也就是将连续的Java堆划分为多个大小相等的独立区域,即Region,它是G1的回收最小单元,每个Region都可以扮演新生代或者老年代的角色,这是不固定的。这些Region中有一类会作为Humongous区域 ,G1会将大小超过Region容量一半的大对象存放在这里。每个Region的大小可以通过参数-XX: G1HeapRegionSize设定, 取值范围为1MB~32MB, 且应为2的N次幂。 而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中, G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1收集器会跟踪各个Region里面的垃圾堆积的“价值”大小, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMillis指定, 默认值是200毫秒) , 优先处理回收价值收益最大的那些Region 。
针对对象跨Region问题,每个Region存在一个独有的记忆集,记录别的Region指向自己的指针,并且标注这些指针分别在哪些卡页范围之内。由于Region数量比传统收集器的分代数量明显要多得多, 因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。 根据经验, G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
用户通过-XX: MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值 ,针对如何计算出这个期望值,G1在垃圾收集过程中, 会根据历史数据,计算出预测期望,预测值期望=衰减平均值+(置信度*衰减标准差),采用衰减均值意味着,时间越久,数据的权重越小,即新的数据对衰减均值影响更大。
G1为每一个Region设计了两个名为TAMS(Top at Mark Start) 的指针, 把Region中的一部分空间划分出来用于并发回收过程中的新对象分配, 并发回收时新分配的对象地址都必须要在这两个指针位置以上。默认它们是存活的, 不纳入回收范围。
G1收集器的收集步骤分为:
G1收集器除了并发标记外, 其余阶段也是要完全暂停用户线程的,换言之, 它并非纯粹地追求低延迟, 官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量 。
G1 与 CMS比较而言,优势在于可以指定最大停顿时间,Region布局,不会产生内存空间碎片,劣势在于G1会额外占据较高的负载内存,也就是G1中每个Region都有一个卡表,而CMS只有老年代到新生代的引用需要一个卡表。
CMS使用的是写后屏障来更新维护卡表,G1此之外还需要使用写前屏障来跟踪并发时指针变化情况,这是由于CMS采用的是增量更新方法,G1使用的原始快照方法。
在小内存上CMS往往表现比G1要好,这个平衡点在6-8GB之间。
Shenandoah收集器只存在于OpenJDK,与G1相比,最重要的改变在于支持并发的整理算法,此外Shenandoah没有使用G1中非常耗费资源的记忆集,而是采用了邻接矩阵的方式,记录跨Region的引用关系。
Shenandoah收集器工作被拆成九个阶段,其中并发标记、 并发回收、 并发引用更新是最重要的
初始标记:与G1一样, 首先标记与GC Roots直接关联的对象, 这个阶段仍是“Stop The World”的
并发标记:与G1一样, 遍历对象图, 标记出全部可达的对象, 这个阶段是与用户线程一起并发的
最终标记:与G1一样, 处理剩余的SATB扫描, 并在这个阶段统计出回收价值最高的Region, 将这些Region构成一组回收集(Collection Set) 。 最终标记阶段也会有一小段短暂的停顿。
并发清理:这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region
并发回收:在这个阶段, Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。这一阶段由于是与用户线程并发执行,因此移动对象的同时, 用户线程仍然可能不停对被移动的对象进行读写访问, 移动对象是一次性的行为, 但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址, 这是很难一瞬间全部改变过来的。 这个问题将使用读屏障和Brooks Pointers转发指针来解决。
初始引用更新:这阶段负责见建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务
并发引用更新:并发引用更新与并发标记不同, 它不再需要沿着对象图来搜索, 只需要按照内存物理地址的顺序, 线性地搜索出引用类型, 把旧值改为新值即可。
最终引用更新:解决了堆中的引用更新后, 还要修正存在于GC Roots中的引用。 这个阶段是Shenandoah的最后一次停顿
并发清理:回收回收集中所有的的Region空间
黄色的区域代表的是被选入回收集的Region, 绿色部分就代表还存活的对象, 蓝色就是用户线程可以用来分配对象的内存Region了
Brooks Pointers转发指针
它在原有的对象布局中统一增加一个新的引用,在正常不处于并发移动的情况下, 该引用指向对象自己。转发指针加入后带来的收益自然是当对象拥有了一份新的副本时, 只需要修改一处指针的值, 即旧对象上转发指针的引用位置, 使其指向新对象, 便可将所有对该对象的访问转发到新的副本上。 这样只要旧对象的内存仍然存在, 未被清理掉, 虚拟机内存中所有通过旧引用地址访问的代码便仍然可用, 都会被自动转发到新对象上继续工作,
但是存在三种并发场景:
1) 收集器线程复制了新的对象副本; 2) 用户线程更新对象的某个字段; 3) 收集器线程更新转发指针的引用值为新副本地址。
如果2在1、3之前发生的话,就会导致用户线程对对象做出的更改在旧对象上,因此这里需要进行同步操作措施(CAS)。
为了实现Brooks Pointer, Shenandoah在读、 写屏障中都加入了额外的转发处理,由于代码中往往对于对象的读比写频率要多的多,因此读屏障会带来巨大的开销,因此在JDK13中,将Shenandoah的内存屏障模型改进为基于引用访问屏障,仅拦截引用类型的读写操作,而不用管原生数据类型的读写。
ZG收集器是一款基于Regio内存布局的,暂时不设分代的收集器,它仅使用了读屏障,没有使用写屏障,此外它还引入了染色指针,内存多重映射技术来实现可并发的标记-整理算法。
内存布局中Region分为三类容量:
在对象标记过程中,Serial收集器是直接在将标记记录在对象头上,G1和Shenandoah使用BitMap的数据结构存储,而ZGC使用染色指针技术,直接记录在引用对象的指针上。
由于一些技术的原因,在64位寻址的操作系统上,往往不能全部利用上,AMD64架构只使用了48位的虚拟地址空间,Linux只使用46位,windows只使用44位。
以Linux系统为例,ZGC将46位指针宽度中最高的四位用于存储对象的三色标记、是否进入冲分配集、是否只能通过finalize()方法才能被访问,因此ZGC能管理到的内存不可以超过42位。
使用染色指针的好处
但是如何让操作系统只读取染色指针的地址位,忽略标志位,这将使用内存映射技术来实现。、,将多个不同的虚拟内存地址映射到同一个物理地址上,这样讲标志位看做是不同的分段,映射到的还是同一片物理内存空间。