JVM都有哪些垃圾回收算法?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM都有哪些垃圾回收算法?相关的知识,希望对你有一定的参考价值。
参考技术A 标记-清除,标记-复制,标记-整理 参考技术B 1.Mark-Sweep(标记-清除)算法这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
2.Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
3.Mark-Compact(标记-整理)算法(压缩法)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 参考技术C
1.标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
适用场合:
存活对象较多的情况下比较高效
适用于年老代(即旧生代)
缺点:
容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
2.复制算法
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
现在的商业虚拟机都采用这种收集算法来回收新生代。
适用场合:
存活对象较少的情况下比较高效
扫描了整个空间一次(标记存活对象并复制移动)
适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少
缺点:
需要一块儿空的内存空间
需要复制移动对象
3.标记整理
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。
这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
4.分代收集算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
垃圾回收机制
根据直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置所说,年轻代分为Eden区和survivor区(两块儿:from和to),且Eden:from:to==8:1:1。
jvm内存结构
1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);
2)当Eden区满了或放不下了,这时候其中存活的对象会复制到from区。
这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉。
3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存。
4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。
5)当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)。
垃圾回收有两种类型:Minor GC 和 Full GC。
1.Minor GC
对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
2.Full GC
也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
垃圾回收算法总结
1.年轻代:复制算法
1) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
4) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
2.年老代:标记-清除或标记-整理
1) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
以上这种年轻代与年老代分别采用不同回收算法的方式称为"分代收集算法",这也是当下企业使用的一种方式
3. 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。
JVM专题--垃圾回收算法, 垃圾回收器
前言
Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C、C++程序,需要程序猿手动释放内存,Java则不需要,是由垃圾回收器去自动回收。垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。于是诞生了很多算法, 基于这些算法也有不同的垃圾收集器, 我们将在本文中详细讲解这些垃圾回收算法和垃圾回收器.
垃圾判断算法
判断JVM中的对象, 哪些是存活的, 哪些是可以被回收的算法
引用计数算法
最简单的垃圾判断算法, 为对象添加一个属性, 标记对象被引用的次数, 被引用的话, 计数器+1, 引用失效时, 计数-1, 当计数器等于0时, 表示没有被其他对象引用, 可以进行回收. 但是当出现循环引用的话, 对象会无法被回收. 如下所示:
可达性分析
通过一些列被称为”GC Roots”的对象为根节点集, 通过这些根节点通过引用向下搜索, 如果某个对象不能被搜索到, 则表明这个对象没有被引用, 可以进行回收, 反之表明这个对象是一个存活对象, 不可被回收.
JVM中使用的垃圾判断算法就是可达性分析, 先对能搜索到的对象打个标记, 垃圾回收时回收没有标记的对象
哪些对象可以作为”GC Root”?
所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值.
方法区中类静态属性引用的对象.
方法区中的常量引用的对象
本地方法栈中JNI(native方法)引用的对象
垃圾回收算法
标记-清除算法
GC会分为两个阶段, 标记和清除, 对可回收的对象进行标记, 待标记完成时, 会对进行标记的对象进行回收, 标记-清除会产生空间碎片, 导致大对象分配内存时, 没有连续空间而导致再次触发GC, 标记-清除的效果如图所示:
标记-整理算法
也会分为两个阶段, 第一步先标记可回收的对象, 然后将存活对象向一端移动, 最后清除边界以外的对象, 标记整理算法会解决标记清除算法的空间碎片问题, 存在对象的移动过程, 效率会比标记清除低, 标记-整理的效果如图所示:
复制算法
复制算法会将内存区域分为两块, 每次只使用一块, 当使用的那块内存满了时, 会将存活对象移动到另一块内存区域, 然后直接清空当前使用内存区域. 因为是对半块内存的回收, 所以复制算法效率很高, 也不会存在空间碎片问题, 缺点就是需要两倍的内存空间, 复制算法效果如图所示:
分代收集算法
根据对象的存活周期, 可以对堆区新生代和老年代使用不同的垃圾回收算法, 由于新生代对象经过垃圾回收后, 会有大量的对象被回收, 存活对象较少, 所以一般采用复制算法, 而老年代的对象存活率高, 没有额外的过多内存分配, 所以一般采用标记-清除或者标记-整理算法
三色标记与读写屏障
三色标记
前面我们看到, 不论是哪种垃圾回收算法, 都少不了标记, 垃圾回收器的工作基本上分为两步1.标记存活对象, 2.回收未被标记的对象, 那么JVM是如何标记一个存活对象呢?
根据可达性分析, 我们要找出存活对象, 需要从GC Roots开始遍历, 可达的就是存活对象, 在遍历的时候, 对象会被标记为以下三种颜色:
白色:尚未访问过。
黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
如图所示:
A对象已访问过, 并且引用的B对象也访问过了, A会被标记为黑色, B对象已访问, 可能还有其他引用对象未访问, 此时为灰色, 如果其他引用都访问过, B会被标记为黑色, C对象尚未访问过, 被标记为白色.白色是垃圾回收时, 要被回收的对象
读写屏障
读写屏障, 就是在对象的读写操作之前做一些处理, 可以参考Spring AOP的理念:
读屏障
即在读前做一些处理
读屏障()
读操作
写屏障
即在写前后做一些处理
写前屏障()
写操作
写后屏障()
三色标记会出现以下三种特殊情况:
多标
如图所示, 用户线程标记了A对象对B的引用, GC线程执行垃圾回收的时候, A断开了对B的引用, 这时候B对象及B对象所引用的对象存在多标的情况, 在此次垃圾回收的时候不会被回收, 可能会在下次垃圾回收的时候进行回收
少标
如图所示, 在GC线程执行过程中, 用户线程创建了新的对象, 因为新创建的对象都是黑色, 如果存在新创建的对象使用后就失去引用了, 那么这些对象就会出现少标的情况, 在此次垃圾回收的时候, 不会进行回收, 可能会在下次GC时进行回收
漏标
如图所示, 在GC执行过程中, A节点开始没有对D的引用, 所以扫描完B之后A被标记为黑色, 然后扫描B的过程中, B断开了对D的引用, 同时A建立了对B的引用,因为A已经被扫描过标记为黑色, 不会被重复扫描, D就是一个被漏标对象, 此次垃圾回收会被回收掉, 导致程序出现异常.
如何解决漏标的问题?:
从之前案例中可以分析出, 漏标出现的情况有两个必要条件
灰色对象断开了对白色对象的引用; 即灰色对象的成员变量发生变化
黑色对象重新对白色对象建立引用; 即黑色对象重新引用成员变量
1.读屏障+重新标记
在建立A对D的引用时, 把类似的对象全部标记为灰色或者白色缓存到一个集合中, 然后STW, 等待标记完成后, 重新标记集合中的对象
重新标记环节, 一定要STW, 不然可能会没完没了的标记下去
2.写屏障+增量更新
这种方式是在解决条件2, 即通过写屏障记录下更新, 具体操作是在A对象建立对D对象的引用时, 通过写屏障, 把D对象添加到待扫描的集合中等待扫描
3.写屏障+原始快照
这种方式是在解决条件1, 使扫描时依然能够扫描到D, 具体操作是, 在B断开D的引用时, 把B对D的引用关系保存起来, 在扫描的时候, 扫描旧的对象图,这个旧的对象图就是原始快照
4.实际应用
CMS:写屏障+增量更新
G1:写屏障+原始快照
串行, 并行, 并发
串行:一个GC线程运行
并行:多个GC线程运行
并发:多个GC线程和多个用户线程同时进行(即使是并发收集器, 也会在某些阶段出现STW)
STW
Stop The World, 即GC线程和用户线程是不能同时运行的阶段, 不管事并行还是并发, 总会有阶段出现STW
垃圾回收器
Serial收集器
串行垃圾收集器, 用户线程与GC线程交替进行, 垃圾回收的过程中会STW, 垃圾回收完成后才会恢复所有用户线程, 专注于收集年轻代, 底层是复制算法.
相关参数: -XX:+UseSerialGC 新生代使用Serial收集器
执行过程如图所示:
ParNew收集器
并行垃圾收集器, Serial收集器的多线程版本, 唯一能与CMS搭配使用的新生代垃圾收集器
相关参数:
-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器
-XX:+UseParNewGC:强制指定使用ParNew
-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
执行过程如图所示:
Parallel收集器
关注吞吐量的垃圾回收器
吞吐量=用户线程执行时间/(用户线程时间+垃圾回收器执行时间)
相关参数:
-XX:MaxGCPauseMilli:是一个大于0的毫秒数, 垃圾回收器会尽量保证垃圾回收时间小于这个值. 但大家也不要想着把这个值设置的很小, 就能使得垃圾收集的速度就更快, GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的: 比如收集300M肯定要比收集500M要快吧, 但是也提升了垃圾回收的次数, 比如本来回收500M是每10s收集一次, 每次停顿50ms, 而300M则每7s收集一次, 每次停顿30ms, 虽然停顿时间降下来了, 但是对应吞吐量也提升了
-XX:GCTimeRatio:是一个(0-100)的整数, 也就是垃圾收集时间占总时间的比率, 计算公式是1/(1+设置值), 比如设置为19, 那么垃圾收集时间占用比率就是1/(1+19)=5%, 该值默认是99, 也就是默认垃圾收集占用总时间的1%
-XX:+UseAdaptiveSizePolicy:一个开关参数, 当开关打开后, 就不用手动指定新生代大小(-Xmn), Eden区与Survivor区的比例(-XX:SurvivorRatio), 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等细节参数了, java虚拟机会根据当前系统的运行情况, 手机性能监控信息, 来动态的调整这些参数以保证最合适的停顿时间以及最大的吞吐量, 这种调节方式的名称成为GC自适应的调节策略(GC Ergonomics)
执行过程如图所示:
Serial Old收集器
Serial收集器的老年代版本, 基于标记-整理算法实现
有两个作用:
1.与Serial、Parallel收集器配合使用
2.作为CMS收集器的备用方案
Parallel Old收集器
Parallel收集器的老年代版本, 基于标记-整理算法实现
CMS收集器
CMS是我们要重点关注的垃圾收集器, 我们先看下他的执行流程:
CMS收集器聚焦低延迟, 适用于老年代, 基于标记-清除算法实现
从图中我们可以分析下CMS的执行流程:
1.初始标记
会STW,只标记GC Roots直接关联的对象
2.并发标记
不会STW, GC线程用用户线程并发执行.
会沿着GC Roots关联的对象链, 扫描整个对象图, 可想而知整个过程时间会比较长, 但是由于整个过程是不会STW的, 所以不会感觉到卡顿, 只有性能指标会显示CPU飙升
3.重新标记
会STW, 上文说过, 三色标记会出现漏标的问题, CMS收集器就会采用写屏障+增量更新的方式, 将并发标记阶段建立的对象保存起来, 在这个阶段, 就会遍历保存的集合, 防止出现漏标的问题
4.并发清除
不会STW, 用户线程与GC线程并发执行, 清除未标记的对象
默认启动的回收线程数 = (处理器核心数 + 3) / 4
相关参数:
-XX:+UseConcMarkSweepGC:手动开启CMS垃圾收集器
-XX:+CMSIncrementalMode:设置为增量模式
-XX:CMSFullGCsBeforeCompaction:进行多少次垃圾回收后, 执行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示在达到阈值时, 才进行垃圾回收
-XX:CMSInitiatingOccupancyFraction:由于CMS垃圾收集器是并发收集器, 即使在运行阶段, 用户线程仍然在运行, 仍然会产生对象, 所以不能等内存满了才进行垃圾回收, 这个阈值默认是92%
-XX:+UseCMSCompactAtFullCollection:表示CMS收集器在执行完一次垃圾回收后, 是否需要进行内存碎片整理
CMS垃圾收集器并不是完美的(不然怎么会产生后面G1,ZGC…呢), 会有哪些缺点呢?
垃圾回收过程中, 会与用户线程抢夺CPU资源(当然这是所有并发回收器存在的问题);
无法处理浮动垃圾, (少标产生的对象, 即标记结束后产生的对象)
因为采用标记-清除算法, 存在内存碎片
G1收集器
G1收集器可以说与之前的所有收集器都不同, 也可以说是所有垃圾收集器的综合体, 他的实现原理是将堆内存分成了一个一个的Region, 这些Region在使用时才会被赋予角色Eden, From, To, Old, humongous. 每个Region只能被赋予一个角色, 一个Region不能既是Eden又是From.
如图所示:
每个region的大小可通过参数-XX:G1HeapRegionSize设置,取值范围是2-32M.
如果一个对象的大小超过一个Region的一半, 则会被认定为是大对象, 大对象会用N个连续的Region进行存储
G1的名字代表的是Garbage First, 回收某个region的价值大小 = 回收获得的空间大小 + 回收所需时间, G1收集器会维护一个优先级列表, 每个region会按照价值大小, 有序排列在这个列表中, 回收的时候, 会优先回收回收价值较大的region, 这也是G1名字的由来.
G1的执行流程如图所示:
从图中我们可以分析下G1的执行流程:
1.初始标记
会STW, 做了两件事
1.修改TAMS的值, TAMS是一个指向当前Region中并发标记过的位置指针, TAMS值以上的新建对象默认标记为存活对象
2.标记GC Roots直接关联的对象
所以这一步会STW,但是会很快
2.并发标记
不会STW, 会根据GC roots引用链, 遍历整个对象图, 标记存活对象
这个阶段耗时会比较长, 同样因为是与用户线程并发执行的, 所以不会感觉到卡顿, 只是性能上会感觉到CPU飙升
3.最终标记
之前说解决漏标时有种解决方案, 就是扫描原始快照, 即对灰色对象断开白色对象的引用时, 记录引用关系, 并且保存到集合中, 这里就是扫描原始快照旧的引用, 防止出现漏标
4.筛选回收
更新Region的统计数据, 对各个Region回收价值进行计算并排序, 根据用户设置的期望回收的暂停时间, 生成回收集
然后开始执行清除操作, 将旧的Region中的存活对象移动到新的Region中, 这个过程会STW
相关参数
-XX:G1HeapRegionSize:设置Region的大小
-XX:MaxGCPauseMillis:设置垃圾回收是允许的最大暂停时间(默认200ms)
-XX:+UseG1GC:开启G1
-XX:ConcGCThreads:设置并发标记
-XX:ParallelGCThreads:STW期间, 并发执行的GC线程数量
G1垃圾回收器也有一定的缺点
1.同样的做为一款并发执行的垃圾回收器, 在回收过程中, 也会与用户线程竞争CPU资源
2.会占用10%~20%的内存来存储G1收集器运行所需要的数据
以上是关于JVM都有哪些垃圾回收算法?的主要内容,如果未能解决你的问题,请参考以下文章