一句话详情,垃圾回收就是由程序自动的回收?已死?对象。
已死?对象就是程序中肯定不会再被用到的对象。
垃圾回收可以分为两个部分,一是如何判断对象?已死?,二是如何清除掉?已死?对象。
判断对象已死的方法
引使用计数法
引使用计数法比较好了解,就是为每个对象分配一个计数器,当一个对象被另一个对象引使用时,其对应的计数器+1,当引使用关系被解除时,计数器-1。当一个对象的计数器值为0时,则代表该对象可以被回收了。
引使用计数法的优点是实现简单且回收效率高,而缺点就是无法处理循环引使用的问题。
引使用计数法在Python等少量语言中有用到,但jvm并没有采使用,关键起因也是其无法处理循环引使用的问题(那python的循环引使用对象不能被回收?)。
可达性分析
可达性分析是商使用jvm中采使用的判断对象已死算法。
该算法相似于图遍历,我们把所有对象形容为一张图,节点是对象,边是引使用关系。
从?GC ROOT?节点出发,遍历所有节点,对于遍历到的每个节点都做一个标识,遍历完成后。没有标识的节点说明是可回收的。
这里的?GC ROOT?在JVM中指的是以下几类对象:
被栈中的本地变量表引使用的对象
被静态变量引使用的对象
被常量引使用的对象
被JNI方法中引使用的对象
回收算法
上一节详情了垃圾回收第一步:判断对象能否可以被回收,这一小节则会阐述少量常使用的回收算法。
标记清理
标记清理算法也比较简单。通常用一张?表?(相似)来记录哪些空间已被用。首先通过可达性分析找到所有的?垃圾?,而后将其占使用的空间释放掉。 该算法的问题是可能会产生大量的内存碎片。
标记整理
为理解决内存碎片的问题,标记整理在标记清楚算法上做了优化,在找到所有?垃圾?对象后,不是直接释放掉其占使用的空间,而是将所有存活对象往内存一端移动。回收完成后,所有对象都是相邻的。
复制算法
复制算法将内存区域划分为两个,同一时间只有一个区域有对象,每次垃圾回收时,通过可达性分析算法,找出所有存活对象,将这些存活对象移动到另一区域。为新对象分配内存时,可以通过智能指针的形式,高效简单。 复制算法的缺点是会白费一部分空间以便存放下次回收后存活的对象且需要一块额外的空间进行担保(当一个区域存放不下存活的对象时)。
分代收集
在商使用jvm中,大多用的是分代收集算法。 根据对象的特性,可以将内存划分为3个代:年轻代,老年代,永久代(jvm8后称为元空间)。年轻代存放新分配的对象,用的是复制算法,老年代用标记清理or标记整理算法。其中年轻代分为一个Eden区和两个Survivor区,其比例默认为8:1:1(-XX:SurvivorRatio),新对象在Eden区分配,当Eden区存放不下时,触发一次Young GC,将Eden区和一个Survivor区域的所有存活对象拷贝到另一个Survivor区域。假如对象大小超过肯定大小(-XX:PretenureSizeThreshold),则会直接在老年代分配。老年代采使用标记清理或者标记整理算法。
年轻代老年代比例默认为3:8(-XX:NewRatio,-Xmn),老年代一般来说要比年轻代要大,由于当年轻代空间不足以存放下新对象时,需要老年代来担保。
年轻代用复制算法的起因是年轻代对象的创立和回收很频繁,同时大部分对象很快都会?死亡?,所以复制算法创立和回收对象的效率都比较高。
老年代不用复制算法的起因是老年代对象通常存活时间比较长,假如采使用复制算法,则复制存活对象的开销会比较大,且复制算法是需要其余区域担保的。 所以老年代不用复制算法。
垃圾回收器
下文将详情jvm中常使用的垃圾回收器
Serial串行回收器(年轻代)
用单线程,复制算法实现。在回收的整个过程中需要Stop The World。在单核cpu 的机器上,用单线程进行垃圾回收效率更高。
用方法:-XX:+UseSerialGC
ps:在jdk client模式,不指定VM参数,默认是串行垃圾回收器
Serial Old串行回收器(老年代)
与Serial类似,但用标记整理算法实现。
ParNew并行回收器(年轻代)
Serial的多线程形式, -XX:+UseParNewGC(新生代用并行收集器,老年代用串行回收收集器)或者者-XX:+UseConcMarkSweepGC(新生代用并行收集器,老年代用CMS)。
Parallel Scavenge 基于吞吐量的并行回收器(年轻代)
多线程的回收器,高吞吐量(=程序运行时间/(程序运行时间+回收器运行时间)),可以高效率的利使用CPU时间,尽快完成程序的运算任务,适合后端应使用等对响应时间要求不高的场景。
有一个自适应条件参数(-XX:+UseAdaptiveSizePolicy),当这个参数打开后,无需手动指定新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio)等参数,虚拟机会动态调节这些参数来选择最适合的停顿时间(-XX:MaxGCPauseMillis)或者吞吐量( -XX:GCTimeRatio)。
Parallel Scavenge是Server级别多CPU机器上的默认GC方式,也可以通过-XX:+UseParallelGC来指定,并且可以采使用-XX:ParallelGCThread来指定线程数。
Parallel Scavenge对应的老年代收集器只有Serial Old和Parallel Old。不能与CMS搭配用的起因是,其用的框架不同,并不是技术起因。
Parallel Old 基于吞吐量的并行回收器(老年代)
用多线程和“标记-整理”算法。与Parallen Scavenge类似,只不过是运使用于老年代。
CMS 关注暂停时间的回收器 (老年代)
基于标记清理算法实现,关注GC的暂停时间,在注重响应时间的应使用上用。
在说CMS具体步骤前,我们先看下CMS用的垃圾标记算法:三色标记法
三色标记法
将堆中对象分为3个集合:白色、灰色和黑色
给大家推荐一个Java技术交流学习群:855355016,欢迎大家加入,群内会有架构视频、笔记、源码等资料。
白色集合:需要被回收的对象
黑色集合:没有引使用白色集合中的对象,且从?GC ROOT?可达。该集合的对象是不会被回收的
灰色集合:从根可达但是还没有扫描完其引使用的所有对象,该集合的对象不会被回收,且当其引使用的白色对象一律被扫描后,会将其加入到黑色集合中。
一般来说,会将被?GC ROOT?直接引使用到的对象初始化到灰色集合,其他所有对象初始化到白色集合,而后开始执行算法:
1.将一个灰色对象加入到黑色集合
2.将其引使用到的所有白色对象加入到灰色集合
3.重复上述两步,直到灰色集合为空
该算法保证从?GC ROOT?出发,所有没有被引使用到的对象都在白色集合中,所以最后白色集合中的所有对象就是要回收的对象
CMS回收过程
分为4个过程,初始标记,并发标记,重新标记,并发清除。
初始标记: 从?GC ROOT?出发,找到所有被?GC ROOT?直接引使用的节点。此过程需要Stop The World。
并发标记: 以上一步骤的节点为根节点,并发的遍历所有节点。同时会开启Write Barrier.假如在此过程中存在黑色对象添加对白色对象的引使用,则会记录下来。
在这里,我们试想下假如三色标记法是先执行步骤2后执行步骤1(上面三色标记算法的步骤),会发生什么?
如下图,在GC过程中,使用三色标记法遍历到A这个对象(图1),将A引使用到的BCD标记为灰色。之后,在应使用程序线程中创立了一个对象E,A引使用了它( 图2这个阶段GC是并发标记的)。而后将A标记为黑色(图3)。在gc扫描结束后,E这个对象由于是白色的,所以将被回收掉。这显然是不能接受的,并发垃圾回收器的底线是允许一部分垃圾暂时不回收(见下面的浮动垃圾),但绝不允许从根可达的存活对象被当作垃圾解决掉!
重新标记: 由于并发标记的过程中可能有引使用关系的变化,所以该阶段需要Stop The World。以?GC ROOT?,?Writter Barrier?中记录的对象为根节点,重新遍历。 这里为什么还需要再遍历?GC ROOT??由于?Writter Barrier?是作使用在堆上的,无法感知到?GC ROOT上引使用关系的变更。
并发清除: 并发的清除所有垃圾对象
CMS通过将步骤拆分,实现了降低STW时间的目的。但CMS也会有以下问题:
1.浮动垃圾,在并发标记的过程中(及之后阶段),可能存在原来被引使用的对象变成无人引使用了,而在这次gc是发现不会清除这些对象的。
2.cpu敏感,由于使用户程序是和GC线程同时运行的,所以会导致GC的过程中程序运行变慢,gc运行时间增长,吞吐量降低。默认回收线程是(CPU数量+3)/4,也就是cpu不足4个时,会有一半的cpu资源给GC线程。
点击链接加入群聊【Java进阶高级架构群】:https://jq.qq.com/?_wv=1027&k=5nadDAW
3.空间碎片,标记清理算法都有的问题。当碎片过多时,为大对象分配内存空间就会很麻烦,有时候就是老年代空间有大量空间剩余,但没有连续的大空间来分配当前对象,不得不提前出发full gc。CMS提供一个参数(-XX:+UseCMSCompactAtFullCollection),在Full Gc发生时开启内存合并整理。这个过程是STW的。同时还可以通过参数(-XX:CMSFullGCsBeforeCom-paction)来这只执行多少次不压缩的Full GC后,来一次压缩的。
4.需要更大的内存空间,由于是同时运行的GC和使用户程序,所以不能像其余老年代收集器一样,等老年代满了再触发GC,而是要预留肯定的空间。CMS可以配置当老年代用率到达某个阈值时( -XX:CMSInitiatingOccupancyFraction=80 ),开始CMS GC。
在old GC运行的过程中,可能有大量对象从年轻代晋升,而出现老年代存放不下的问题(由于这个时候垃圾还没被回收掉),该问题叫Concurrent Model Failure,这时候会启使用Serial Old收集器,重新回收整个老年代。Concurrent Model Failure一般伴随着ParNew promotion failed(晋升担保失败),处理这个问题的办法就是可以让CMS在进行肯定次数的Full GC(标记清理)的时候进行一次标记整理算法,或者者降低出发cms gc的阈值