一、垃圾收集算法

1.1 分代收集算法

现代商用虚拟机中的gc机制一般都会采用“分代收集策略”,如jdk1.8及之前的hotspotvm中,都会采用分代收集策略,也就是根据对象不同的生命周期将堆空间划分为不同的区域,然后在不同区域中采用不同的垃圾收集算法进行回收工作。
新生代:一般使用复制算法,因为在新生代中的对象几乎绝大部分都是朝生夕死的,每次gc发生后只会有少量对象存活,这种情况下采用复制算法无疑是个不错的选择,付出一定的内存空间开销以及少量存活对象的移动开销,换取内存的整齐度以及可观收集效率,这很明显是个“划得来的买卖”。
年老代:一般采用标-整算法或标-清算法,但绝大多数年老代gc器都会选择采用标-整算法,因为毕竟标-清算法会导致大量的内存碎片产生,在年老代对象分配时,内存不完整可能会导致大对象分配不下而持续触发gc。而标-整算法虽然效率较低,但胜在gc后内存足够整齐,再加上年老代的gc并没有新生代频繁,所以年老代空间采用标-整算法无疑也是个不错的选择。
注:为什么年老代不考虑复制算法呢?
一方面是因为年老代空间中的对象普遍存活率都比较高,第二方面是没有新的空间为年老代做分配担保,所以复制算法是明显并不适合年老代的。
在jdk1.8及之前的jvm中,堆中间一般会按照对象的生命周期长短划分为新生代、年老代两个空间,分别用于存储不同周期的对象。而在新版本的gc器,如g1、zgc中,则摒弃了之前物理内存上分代的思想,在运行时并不会直接将堆空间切分为两块区域,而是将整个堆划分为连续且不同的小区间,每一个小区间都独立使用,独立回收,这种回收策略带来的好处是:可以控制一次回收多少个小区间。
“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
1.2 标记-复制算法

复制算法是为了解决效率问题而出现的,它将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。
这个算法也有缺点,操作的时候内存会缩小为了原来的一半,代价很高;其次,持续复制长生存期的对象会导致回收效果不佳,效率较低。
一般的商用虚拟机会采用这种算法来回收新生代(也称为年轻代)的对象,不过研究表明1:1的比例不是很科学,因此新生代的内存空间被细划分为一块较大的 eden 空间和两块较小的 survivor 空间,每次使用 eden 和其中一块 survivor;每次回收时,将 eden 和 survivor 空间中还存活的对象一次性复制到另外一块 survivor 空间上,最后清理掉之前的 eden 和 survivor 空间。
hotspot 虚拟机默认 eden 和 survivor 区的比例是8 : 1 : 1,期望每次回收后只有不到 10% 的对象存活,如果出现 survivor 空间不够用时,需要依赖老年代进行分配担保。
1.3 标记-清除算法

标记-清除算法如同它的名字一样,分为“标记”和“清除”两个阶段,也是最基础的算法。
之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
在标记阶段会根据可达性分析算法,通过根节点标记堆中所有的可达对象,而这些对象则被称为堆中存活对象,反之,未被标记的则为垃圾对象。然后在清除阶段,会对于所有未标记的对象进行清除。
它是最基础的收集算法,比较简单,但是会带来两个明显的问题:
- 效率问题 (如果需要标记的对象太多,效率不高)
- 空间问题(标记清除后会产生大量不连续的碎片)
1.4 标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
在上面我们提到了复制算法的优点和缺点,针对对象存活率较高的场景,进行大量的复制操作时,效率很低下。如果不想浪费 50% 的空间,当对象 100% 存活时,那么需要有额外的空间进行分配担保。
在 hotspot 虚拟机中,堆空间划分成两个不同的区域:新生代和老年代,目的是为了更有效率的回收对象。新生代的对象存活率低,会优先被回收,如果多次执行依然没有被回收,就会转移到老年代。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。
根据老年代的特点,有人提出了另外一种标记-整理算法,也称为标记-压缩算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界外的内存。
二、垃圾收集器

2.1 serial垃圾收集器

启动参数通过 -xx:+useserialgc -xx:+useserialoldgc 设置使用 serial 和 serial old垃圾收集器。
串行收集器是最基本、发展时间最长、久经考验的垃圾收集器,也是client模式下的默认收集器配置。
串行收集器采用单线程stop-the-world的方式进行收集。当内存不足时,串行gc设置停顿标识,待所有线程都进入安全点(safepoint)时,应用线程暂停,串行gc开始工作,采用单线程方式回收空间并整理内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核cpu的场合。
新生代采用复制算法,老年代采用标记-整理算法。
2.2 serial old 垃圾收集器

serial old是serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是被client模式下的虚拟机使用。如果在server模式下,它主要还有两大用途:
一个是在jdk 1.5及之前的版本中与parallel scavenge收集器搭配使用 ,
另外一个就是作为cms收集器的后备预案,在并发收集发生concurrent mode failure的时候使用。
采用标记-整理算法。
2.3 parnew垃圾收集器

parnew收集器实质上是serial收集器的多线程并行版本。
除了使用多条线程进行垃圾收集之外,其余行为包括serial收集器可用的所有控制参数(例如:-xx:survivorratio、-xx:pretenuresizethreshold、-xx:handlepromotionfailure等)、收集算法、stop the world、对象分配规则、回收策略等都与serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
它更注重最小化停顿时间,以减少垃圾收集对应用程序的影响,因此适合需要较短 gc 暂停时间的应用。
可以通过调整 -xx:parallelgcthreads 来控制并行线程的数量。
专为与 cms 一起使用而设计的年轻代垃圾收集器,是 cms 的默认搭档。因此,在老年代使用 cms 时,parnew 是首选的新生代收集器。
新生代采用复制算法,老年代采用标记-整理算法。
2.4 paraller scavenge垃圾收集器

parallel scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
paraller收集器也是jdk1.8默认的垃圾收集器,年轻代使用paraller scavenge垃圾收集器,老年代使用paraller old垃圾收集器,可通过命令 java -xx:+printcommandlineflags -version 查看。
parallel scavenge收集器的特点是它的关注点与其他收集器不同,cms等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel scavenge收集器的目标则是达到一个可控制的吞吐量(throughput)。
新生代采用复制算法,老年代采用标记-整理算法。
2.5 paraller old垃圾收集器

parallel old收集器在jdk 6中引入,解决了之前parallel scavenge收集器搭配老年代收集器的局限性。
在parallel old出现之前,使用parallel scavenge收集器的新生代,老年代的选择仅限于serial old(也称ps marksweep),单线程的老年代收集限制了垃圾收集的性能和效率,从而导致吞吐量降低。
名副其实的搭配: parallel old的引入为“吞吐量优先”的收集策略提供了完整的解决方案,使得parallel scavenge收集器和parallel old收集器组合成为处理器资源优化的理想选择。
适用场景: 在注重吞吐量或处理器资源相对紧张的应用场景中,parallel scavenge加parallel old收集器的组合是一个优先考虑的选择。
采用标记-整理算法。
2.6 cms垃圾收集器

cms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是hotspot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的mark sweep这两个词可以看出,cms收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程(stw),并记录下gc roots直接能引用的对象,速度很快。
- 并发标记: 并发标记阶段就是从gc roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
- 并发清理: 开启用户线程,同时gc线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
- 并发重置:重置本次gc过程中的标记数据。
其中初始标记、重新标记这两个步骤仍然需要“stop the world”。初始标记仅仅只是标记一下gc roots能直接关联到的对象,速度很快,并发标记阶段就是进行gc roots tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,cms收集器的内存回收过程是与用户线程一起并发地执行的。
cms的相关核心参数
- -xx:+useconcmarksweepgc:启用cms
- -xx:concgcthreads:并发的gc线程数
- -xx:+usecmscompactatfullcollection:fullgc之后做压缩整理(减少碎片)
- -xx:cmsfullgcsbeforecompaction:多少次fullgc之后压缩一次,默认是0,代表每次fullgc后都会压缩一次
- -xx:cmsinitiatingoccupancyfraction: 当老年代使用达到该比例时会触发fullgc(默认是92,这是百分比)
- -xx:+usecmsinitiatingoccupancyonly:只使用设定的回收阈值(-xx:cmsinitiatingoccupancyfraction设定的值),如果不指定,jvm仅在第一次使用设定值,后续则会自动调整
- -xx:+cmsscavengebeforeremark:在cms gc前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低cms gc的标记阶段时的开销,一般cms的gc耗时 80%都在标记阶段
- -xx:+cmsparallellnitialmarkenabled:表示在初始标记的时候多线程执行,缩短stw
- -xx:+cmsparallelremarkenabled:在重新标记的时候多线程执行,缩短stw;
三、使用java -xx:+printcommandlineflags -version查看垃圾收集器
| 垃圾回收方式 | 代表垃圾回收器 |
|---|---|
| useserialgc | serial + serial old |
| useparnewgc | parnew + serial old |
| useconcmarksweepgc | parnew + cms |
| useparallelgc | parallel scavenge + parallel old |
| useparalleloldgc | parallel scavenge + parallel old |
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论