垃圾收集器与内存分配策略
并发的可达性分析
三色表记法:主要解决或者降低GC过程中用户线程的停顿,把遍历对象图过程中遇到的对象按照"是否访问过"这个条件标记为三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过
标记过程在与用户线程并发执行过程中会产生"对象消失"的问题,同时满足以下两个条件时,会出现这种问题:
-
赋值器插入了一条或多条从黑色对象到白色对象的新引用
-
复制器删除了全部从灰色对象到白色对象的直接引用或间接引用
因此要解决并发扫描过程中对象消失的问题,只需要破坏两个条件的任意一个即可,由此分别产生了两种方案:
-
增量更新:当黑色对象插入新的指向白色对象的引用关系时,将这个新插入的引用记录下来,等并发扫描结束后,以记录的黑色对象为跟重新扫描一次(破坏第一个条件)
-
原始快照:当灰色对象删除白色对象的引用关系时,将这个删除的引用记录下来,并发扫描结束后以记录的灰色对象为跟再次重新扫描
实际应用:CMS是基于增量更新来做并发标记的,G1和Shenandoah则是使用原始快照来实现的
Java垃圾回收器的发展历史
第一阶段:串行垃圾回收器:jdk1.3.1之前Java虚拟机仅仅只支持Serial收集器
第二阶段:并行垃圾回收器:随着多核的出现,Java引入了并行垃圾回收器,充分利用多核性能提升垃圾回收效率
第三阶段:并发标记清理回收器CMS:垃圾回收器可以和应用程序同时运行,降低暂停用户线程执行的时间
第四阶段:G1(并发)回收器:初衷是在清理非常大的堆空间的时候能满足特定的暂停应用程序的时间,与CMS相比会有更少的内存碎片
垃圾回收算法
垃圾回收器(GC:Garbage Collection)的3个任务:分配内存、确保被引用对象不被错误回收、回收不再被引用的对象的内存空间
- 引用计数算法Reference Counting Collector:简单效率低,通过在堆中对每个对象都有一个计数器,当对象被引用时+1,当引用被置空或者离开作用域时-1;这种方式无法解决互相引用问题,JVM没有采用
- 追踪回收算法Tracing Collector:利用JVM维护的对象引用图,从根节点出发开始遍历,同时标记遍历过的对象,遍历结束后没有被标记的说明可以被回收
- 压缩回收算法Compacting Collector:把堆中活动的对象转移到一端,另一端会空出很大的空闲区域,对堆中的碎片进行了处理,但带来了性能的损失
- 拷贝回收算法Coping Collector:把堆分为2个大小相同的区域,任何适合都只使用其中一个,直到这个区域消耗完为止,此时垃圾回收器中断程序执行,通过遍历的方式将对象拷贝到另一区域,拷贝时也是相邻布置的,拷贝结束后程序继续运行,直到这块区域被使用完再采用其他方式进行垃圾回收;优点是消除了内存碎片,缺点是需要两倍堆空间大小的内存,而且内存调整中断程序降低程序执行效率
- 按代回收算法Generational Collector:缺点是执行时所有处于活动的对象都要拷贝,效率低;可根据“程序中大部分对象的生命周期都很短”的特点进行优化;
串行垃圾回收器
单线程垃圾回收器,运行时会暂停所有应用线程,Stop The World(STW),不适用与服务器环境中,适用在客户端程序中;
可以使用JVM参数-XX:+UseSerialGC来指定使用串行垃圾回收器
年轻代使用的是拷贝算法,老年代或永久代使用的是标记-清扫-压缩算法
标记-清扫-压缩:标记阶段识别哪些对象存活;扫描阶段会扫描整个代,识别出哪些是垃圾;压缩阶段执行平移压缩,存活的移动到最前端,尾部流出连续空闲空间。
之后的分配就可以在老年代或永久代使用空指针算法(指针碰撞)
指针碰撞(bump-the-pointer)算法:JVM维护2个指针(allocatedTail指向已分配对象的尾部,geneTail指向代尾)当需要分配内存是由两个指针可判断剩余空闲空间是否够用,如果够则更新allocatedTail指针来把内存分配给请求的对象
另一种分配内存的方法是空闲列表:记录了哪块内存是可用的以及对应大小
具体采用哪种方式由Java堆是否规整来决定的,是否规整又是由采用的垃圾收集器是否带有空闲压缩整理的能力决定
并行垃圾回收器
并行垃圾回收运行时仍然会暂停应用程序,但使用了多线程能够缩短垃圾回收的时间
设置使用的垃圾回收算法:-XX:+UseParallelGC 在单核CPU上设置无效
在一台有N个CPU的主机上并行垃圾回收器会使用N个垃圾回收线程进行垃圾回收,也可通过参数指定:-XX:ParalleGCThreads=<垃圾回收器线程个数>
并发标记清理回收器CMS
最大并发量的标记清除垃圾回收器:Concurrent Mark Sweep(老年代收集器);年轻代使用拷贝算法,老年代使用最大并发量的标记清除算法(避免清理老年代暂停用户程序太长时间)
主要通过以下两种方法来实现避免用户程序有太长时间的停顿:
- 使用空闲链表来管理和回收的空间,而非压缩老年代内存;
- 将多数的标记清理工作和应用程序并发执行。
这种算法花费了大量的时间在标记-清理阶段,这个阶段的认为可与用户线程并发执行,不过仍与用户线程竞争CPU资源,默认下CMS使用的线程数等于(机器物理内核数量 + 3)/4,也即是如果核心数是4个或以上时,保证只占用不超过25%的CPU资源
可使用参数:-XX:UseConcMarkSweepGC显示指定使用CMS算法。
CMS的执行步骤:初始标记->并发标记->重新标记->并发清除
- 初始标记:标记在老年代由Root集直接可达或被年轻代引用的对象,这一步会暂停用户线程的执行;
- 并发标记:遍历老年代,由上一步标记的节点开始标记所有被引用的对象,并发执行所以并不会标记所有的被引用对象;
- 并发预清理:与应用程序并发执行,由于上一步执行中有些引用可能发生了变化而导致标记不准确,这一步将这些引用发生变化的节点标记为dirty,对从dirty节点出发的可达节点进行标记;
- 并行的可被终止的预清理(CMS-concurrent-abortable-preclean):与应用程序并发执行,也是执行一些预清理;
- 重标记:暂停应用程序的执行,最终确定老年代中所有存活的对象;
- 并行清理:与应用程序并发执行,删除不再使用的对象从而回收被他们占用的内存空间;
- 并发重置:重置数据结构为下一次运行做准备。
这种回收算法与应用程序的并发执行大大减少了暂停应用程序的时间,适用于在多核机器上使用,与并行垃圾回收器相比CMS通常在CPU密集的应用程序中有更低的吞吐量。
缺点:
- 会占用一定的CPU资源:核心数是4个或以上时,保证只占用不超过25%的CPU资源
- 无法处理"浮动垃圾",在并发标记和并发清理阶段用户线程还在继续运行,自然会产生新的垃圾对象,这部分是在标记结束后,当次收集无法处理掉,只好在下一次垃圾收集时清理掉,也因此可能会导致另一次的STW的GC产生
- CMS是基于标记-清除算法,会导致内存碎片问题,如果碎片无法容纳新对象也会产生新的GC
G1
G1(Garbage-First)基于标记-整理算法,解决了垃圾回收器暂停用户线程时间的不确定性,它是面向服务器的垃圾回收器,主要针对配备多核CPU及大容量内存的机器,在以极高的概率满足GC暂停用户线程的同时还具有很高的吞吐量,主要有以下几个特点:
- 可预测性:可以预测暂停用户线程的时间,提供了设置暂停时间的选项;
- 压缩特性:在满足暂停时间的要求上尽可能多地消除碎片;
- 并发性:与CMS一样,GC操作可与应用线程一起并发执行;
- 节约:不需要请求更大的Java堆。
与之前的垃圾回收器相比,G1可被看作是一种增量式的并行压缩GC算法,它提供了可以预测暂停时间的功能。通过并行、并发和多阶段标记循环,G1可被应用在堆空间更大的场景,同时还提供合理的在最坏情况下的暂停时间。它的基本思想是在GC工作前设置堆范围(-Xms用来设置堆的最小值,-Xmx设置堆的最大值)和实际暂停目标时间(使用-XX:MaxGCPauseMillis来设置)
G1将年轻代、老年代的物理空间划分取消了,它将堆空间划分为若干个区域Region,一段连续的堆空间被划分为固定大小的区域,然后用一个空闲链表来维护,每个区域要么对应老年代,要么对应年轻代,根据实际堆空间的大小这些区域可被划分为1MB~32MB,从而可以保持总的区域个数在2048左右。G1最主要的原则是:在标记阶段完成后,G1就可以知道哪些heap区的empty空间最大,它会在满足暂停时间的基础上优先回收空闲区域最大的区域,因此它也被称为garbage-first(垃圾优先)的垃圾回收器
虽然引入了区域的概念,但G1本质上仍然属于分代回收器,年轻代的垃圾回收依然会暂停应用线程的执行,它会把存活对象拷贝到Survivor或者老年代;在老年代中G1通过把对象从一个区域复制到另外一个区域来实现垃圾清理的工作,好处是压缩了堆内存,避免了CMS的那种内存碎片的问题,而在G1中这些区域可以是不连续的;
除了年轻代的Eden区、Survivor区和老年代的Old,G1还引入了一种特殊的区域:Humongous区域;这些区域被设计为存放占用超过分区容量50%以上的那些对象,它们被保存在一个连续的区域集合里;对于很大的对象默认会被分配到老年代,如果该对象的生命周期较短则会对垃圾回收器的性能造成很大的影响,Humongous区域就是为了解决这个问题,如果一个H区无法容纳那么G1会寻找连续的H区来存储;
在老年代的回收算法主要分为以下几个步骤:初始标记->并发标记->最终标记->筛选回收
- 初始标记:G1对跟进行标记,主要标记那些可能有引用对象的O区,会暂停应用程序执行
- 跟区域扫描:G1在上一步的基础上扫描对老年代的引用,并标记被引用的对象,不会暂停应用程序的执行,但只有完成该阶段后才能开始下一次的STW年轻代垃圾回收;
- 并发标记:G1在整个堆区域查找存活对象,不会暂停用户程序,而且该阶段可以被STW年轻代垃圾回收中断;
- 重新标记阶段:G1在这个阶段会清空SATB(Snapshot-At-The-Beginning)缓冲区,跟踪未被访问的存活对象,并执行引用处理。如果发现某个region上的所有对象都不再被引用了那么它们将会被直接清除,该阶段会暂停应用程序的执行。
- 清理阶段:这个阶段会执行统计和RSet(Remembered Sets:用来跟踪指向某个heap区内的对象引用,堆内存的每个区都会有一个RSet)的净化操作,在统计过程中G1会识别出那些完全空闲的区域和可以进行混合垃圾回收的区域,此阶段只有一个操作为并发操作:将空白区域重置,并且返回空闲链表。因此该阶段也会触发STW,暂停应用程序的执行
G1是如何建立可靠的停顿预测模型的?
用户通过参数
-XX:MaxGCpauseMillis制定的停顿时间只意味着垃圾收集器发生之前的期望值,G1的停顿预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中G1收集器会记录每个Region的回收耗时,每个Region记忆集里的脏卡数量等各个可测量步骤花费的成本,并分析计算出平均值,标准偏差,置信度等统计信息,然后预测从现在开始回收,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获取最高的收益(这些是最后的筛选回收阶段计算的)G1 的缺点:相比CMS
- 产生的内存占用更高:G1和CMS都使用卡表来处理跨代指针,但GC的更为复杂,堆中的每个Region无论是作为新生代还是老年代都需要维护一份卡表,这部分数据可能会占用整个堆容量的20%乃至更高的内存空间
- 运行时额外的执行负载也更高:CMS使用写后屏障来维护卡表,而G1为了实现原始快照算法还需要写前屏障来记录引用的变化,而且G1实现了类似消息队列的结构来异步处理的写前和写后屏障要做的事情,CMS是同步处理的写后屏障
G1与CMS相比,在大内存(平衡点在6~8G)下G1更具备优势

浙公网安备 33010602011771号