java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > 垃圾收集器与内存分配

java垃圾收集器与内存分配策略详解

作者:会编程的老六

本篇文章主要介绍了Java垃圾收集器与内存分配策略的方法和原理总结,Java垃圾回收器是Java虚拟机的重要模块,具有一定的参考价值,有兴趣的可以了解一下

1.经典垃圾收集器

1.1 Serial收集器

这个收集器是一个单线程工作的收集器,但它的单线程的意义并不仅仅是说明他只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要对的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

目前已经老无可用,但有着优于其他收集器的地方:简单而高效

1.2 ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本。因为它是除了Serial收集器之外,目前唯一可以与CMS收集器配合工作的收集器,所以在JDK7之前的遗留系统中被作为首选的新生代收集器

CMS收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,首次实现了让垃圾收集线程与用户线程同时工作。但是当选用CMS作为老年代收集器时,新生代收集器只能选择使用Serial收集器或者ParNew收集器

随着垃圾收集器技术的不断改进,G1收集器带着CMS继承者和代替者的光环登场。G1收集器是一个面向全堆的收集器,不需要其他新生代收集器的配合工作

1.3 Parallel Scavenge 收集器

Parallel Scavenge收集器也是一款新生代收集器,同样是基于标记-复制算法实现的收集器,也可以并行收集的多线程收集器。它的特点是它的关注点与其他收集器不同。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

$$

吞吐量=\frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间}

$$

提供了两个参数用于精确控制吞吐量:

-XX:MaxGCPauseMillis 参数控制最大垃圾搜集停顿时间,允许的值是一个大于0的毫秒数。收集器将尽力保证内存回收花费的时间不超过用户的设定值。但是设定过分小的值并不能起到加快回收花费的速度的作用。

-XX:GCTimeRatio 参数直接设置吞吐量大小,允许的值是一个大于0小于100的整数。也就是垃圾收集时间占总时间的比率。相当于吞吐量的倒数。

Parallel Scavenge 收集器还有一个参数:-XX:+UseAdaptiveSizePolicy 这是一个开关参数,当这个参数被激活以后,就不需要人工指定新生代的大小,Eden与Survivor区的比例等等。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数。

1.4 Serial Old 收集器

Serial Old 是 Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。可能有两种用途:1. 在JDK5以及之前的版本中与Parallel Scavenge收集器搭配使用 2. 作为CMS收集器发生失败时的后备预案。

1.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现,从JDK6版本开始提供。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

1.6 CMS 收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。整个运作过程分为4步:

步骤名称 行为
初始标记(CMS initial mark) 标记一下GC Roots能直接关联到的对象,需要Stop The World
并发标记(CMS concurrent mark) 从GC Roots的直接关联对象开始遍历整个对象图的过程,可以与垃圾收集线程一起并发运行
重新标记(CMS remark) 修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要Stop the World
并发清楚(CMS concurrent sweep) 清理删除掉标记阶段判断的已经死亡的对象,可以与用户线程同时并发完成

CMS收集器存在三个缺点:

1.CMS收集器对处理器资源非常敏感,默认启动的回收线程数为(处理器核心数量+3)/ 4。在并发阶段会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。

为了缓解这种情况虚拟机提供了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)作用是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集器线程的独占资源的时间,这样整个垃圾收集的过程会更长,但是对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显。效果一般从jdk7开始被声明为deprecated ,从JDK9发布后被完全废弃

2.由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现"Concurrent Mode Failure" 失败进而导致另一完全"Stop The World"的Full GC的产生。

可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获得更好的性能。如果设置的太高将会很容易导致大量的并发失败产生,性能反而降低

3.由于基于标记-清除算法,可能在收集结束时会有大量的空间碎片产生

通过调节:-XX:+UseCMSCompactAtFullCollection开关参数,默认是开启的,从jdk9开始废弃

*** -XX:CMSFullGCsBeforeCompaction 默认值是0,表示每次进入Full GC时都进行碎片整理***

1.7 Garbage First 收集器

Garbage First 收集器,简称 G1收集器,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。是一款主要面向服务端应用的垃圾收集器。可以面向堆内存的任何部分来组成回收集,衡量的标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。G1开创的基于Region的堆内存布局是它能够实现这个目标的关键,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了Region容量一半的对象就可以判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB。

G1收集器之所以可以建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个JAVA堆中进行全区域的垃圾收集。更具体的思路是让G1收集器区跟踪各个Region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需要的时间的经验值,然后在后台 维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis)优先处理回收价值收益最大的那些Region。

G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。G1收集器通过原始快照(SATB)算法实现了保证其不能打破原本的对象图结构的目的。

G1收集器运作过程大致分为四个步骤:

步骤 行为
初始标记(Initial Marking) 标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值。这个阶段需要停顿线程,而且是借用进行Minor GC的时候同步完成的
并发标记(Concurrent Marking) 从GC Root开始对堆种对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,可以与用户程序并发执行。对象图扫描完成以后,还需要重新处理SATB记录下的在并发时有引用变动的对象
最终标记(Final Marking) 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
筛选回收(Live Data Counting and Evacuation) 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理整个旧Region的全部空间,必须暂停用户线程

2低延迟垃圾收集器

2.1 Shenandoah收集器

Shenandoah收集器是一款只有OpenJDK才会包含的。与G1收集器相比,它们两者有着相似的堆内存布局,在初始标记、并发标记等许多阶段的处理思路上都高度一致。但是在管理内存堆方面,与G1收集器至少有三个方面的明显的不同之处:

1.支持并发的整理算法:G1的回收阶段是可以多线程并行的,但不能与用户线程并发。Shenandoah后面会讲到。

2.Shenandoah收集器默认不使用分代收集。

3.Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系。降低了处理跨代指针的记忆集维护消耗,也降低了伪共享问题发生的概率

Shenandoah收集器大致工作流程可以分为9个阶段:

步骤名称 动作
初始标记(Initial Marking) 标记与GC Roots直接关联的对象,这个阶段是Stop The World的,停顿时长与堆大小无关,与GC Roots的数量相关。
并发标记(Concurrent Marking) 遍历对象图,标记出全部可达的对象,这个阶段与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
最终标记(Final Marking) 处理剩余的SATB扫描,在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集,最终标记阶段也会有一小段短暂的停顿。
并发清理(Concurrent Cleanup) 清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
并发回收(Concurrent Evacuation) 核心差异!Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。Shenandoah会通过读屏障和被成称为"Brooks Points"的转发指针来解决在复制对象时遇到的困难。时间长短取决于回收集的大小。
初始引用更新(Initial Update Reference) 把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。在此阶段,只是建立了一个线程集合点,确保所有的并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。时间会很短,有一个十分短暂的停顿。
并发引用更新(Comcurrent Update Reference) 真正开始进行引用更新操作,与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。只需要按照内存物理地址的顺序,线性搜索出引用类型,把旧值改为新值即可。
最终引用更新(Final Update Reference) 解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是最后一次停顿,时间与GC Roots的数量有关。
并发清理(Concurrent Cleanup) 此时整个回收集中所有的Region已再无存活对象,都变成了Immediate Garbage Regions了,最后调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

Brooks Points:Brooks是一个人的名字,它提出使用了转发指针(Forwarding Pointer)的技术来实现对象移动与用户程序并发的一种解决方案。不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。实际上Shenandoah收集器是通过比较并交换(Compare And Swap, CAS)操作来保证并发时对象的访问正确性的。

JDK13中Shenandoah的内存屏障模型改进为基于引用访问屏障(Load Reference Barrier)的实现,所谓“引用访问屏障”是指内存屏障只拦截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写。这能省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗。

2.2 ZGC收集器

ZGC收集器是一款基于Region内存布局的,暂时不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC的Region具有动态性-动态创建和销毁,以及动态的区域容量大小。

染色指针(Colored Pointer):一种直接将少量额外的信息存储在指针上的技术。尽管在linux下64位指针的高18位不能用来寻址,但是剩余的46位所能支持的64TB内存仍然能够充分满足需要。鉴于此,将其高4位提取出来存储四个标记信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集、是否只能通过finalize( )方法才能被访问到。也使得ZGC能够管理的内存不可以超过4TB。使用染色指针的三大优势:

1.可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能被释放和重用掉,不必等待整个堆中所有指向该Region的引用都被修正后才能清理。

2.可以大幅度减少在垃圾收集过程中的内存屏障的使用数量。到目前为止,ZGC都未使用写屏障,只使用了读屏障。

3.可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

Linux/x86-64平台上的ZGC使用了多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内存容量来得更大。把染色指针中的标志位看作是地址的分段符,只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。

ZGC的运作过程(省略部分与之前介绍的G1和Shenandoah相同的小阶段部分):

步骤 动作
并发标记(Concurrent Mark) 遍历对象图做可达性分析的阶段,前后也要经过初始标记、最终标记的短暂停顿。ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1 标志位。
并发预备重分配(Concurrent Prepare for Relocate) 根据特定的查询条件统计得出本次收集过程要清理那些Region,将这些Region组成重分配集。与G1收集器的回收集还是有区别的,ZGC的重分配集只是决定了里面的存活对象会被重新分配复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。
并发重分配(Concurrent Reolcate) 核心阶段!把重分配集中的存活对象复制到新的Region上,并未重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
并发重映射(Concurrent Remap) 修正整个堆中指向重分配集中旧对象的所有引用。

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

您可能感兴趣的文章:
阅读全文