jvm垃圾回收算法和垃圾收集器(Serial、Parallel、Parnew、CMS)
垃圾回收算法
分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
标记-清除算法
这是最简单最基础和直观的垃圾回收算法,他分为两个阶段:
- 标记阶段(Mark): 从一组称为 “GC Roots” 的核心对象(如全局变量、当前执行函数的局部变量等)开始,递归地遍历所有能被访问到的对象,并将它们标记为“存活”。
- 清除阶段(Sweep): 回收器线性遍历整个堆内存,将所有未被标记为存活的对象回收,并将其内存添加到空闲列表中。
优点: - 解决了引用计数算法的循环引用问题。
缺点: - 内存碎片化:回收后会产生大量不连续的内存碎片,可能导致后续无法分配大对象。
执行效率不稳定: - 标记和清除两个过程的效率都会随着堆中对象数量的增加而下降。
标记-复制算法
为了解决碎片化问题,复制算法将可用内存一分为二,每次只使用其中一块。
- 过程: 当正在使用的这块内存(From Space)用完时,就将所有存活的对象复制到另一块空闲的内存(To Space)中,然后一次性清理掉原来的整块内存。
- 复制过程中,对象会被紧凑地排列在To Space,完全消除了内存碎片。
优点:
-
实现简单,运行高效。
-
完全没有碎片问题。
缺点:
- 内存利用率低:任何时候都有一半的内存是空闲的,浪费严重。
- 如果大部分对象都是存活的,复制成本会非常高。
应用:非常适合对象“朝生夕死”(存活率低)的场景,所以常被用作新生代的垃圾回收算法。
标记-整理算法
标记-整理算法是标记-清除算法的一个优化,旨在解决内存碎片问题。
- 标记阶段: 与“标记-清除”算法相同。
- 整理阶段: 将所有存活的对象向内存的一端移动,然后直接清理掉边界以外的所有内存。
优点:
- 避免了内存碎片化。
- 无需浪费一半的内存空间。
缺点:
- 移动对象是一项开销较大的操作,需要更新所有指向这些对象的引用。
- 暂停时间(Stop-The-World Time)通常比复制算法更长。
应用: 通常用于老年代的垃圾回收,因为老年代的对象存活率高,不适合复制。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的Java虚拟机就不会实现那么多不同的垃圾收集器了。
基本概念
在我们学习收集器之前,我觉得我们需要了解以下几个概念:
- Stop-The-World(STW): 停顿时间, 是指我们在执行垃圾收集器的某些任务时,需要暂停所有的用户线程,停顿时间使我们衡量垃圾收集器的重要指标。
- 并行 vs 并发
- 并行: 是指多条垃圾收集线程同时工作,但此时用户线程仍处于等待状态 。
- 并发: 指垃圾收集线程与用户线程同时(或交替)执行 。
serial垃圾收集器
参数:(-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用复制算法,老年代采用标记整理算法
Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Scavenge收集器
参数: -XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
新生代采用复制算法,老年代采用标记整理算法
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。
Parnew收集器
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
新生代采用复制算法
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款以获取最短回收停顿时间为目标的老年低代垃圾收集器,尤其适合重视服务响应速度的应用场景。它基于**“标记-清除**”算法实现,其核心价值在于在垃圾收集的某些阶段,垃圾收集线程可以与用户程序线程并发工作,从而显著减少应用程序因垃圾收集而产生的停顿感。
参数: -XX:+UseConcMarkSweepGC
工作原理:
如上图所示,cms的工作流程大致分为五个步骤:
- 初始标记: 此阶段需要STW。仅仅标记一下GC Roots能直接关联到的对象,速度非常快。
- eg,GC Root先引用了对象A,对象A引用了对象B,那么GC Root和对象A就是直接关联,和对象B就是间接关联。
- **并发标记:**此阶段不需要STW,从GC Roots的直接关联对象开始,遍历整个对象图。这个阶段耗时比较长,但是与用户线程并发进行,不会到导致应用停顿。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 这个阶段主要是标记在初始标记阶段标记的GC Roots相关联的对象,此阶段新产生的GC Root本次不进行处理
- 重新标记: 此阶段需要STW。目的是为了修正在并发标记期间,因用户程序继续运行而导致标记记录产生变动的那一部分对象。这个阶段的停顿时间通常比初始标记稍长,但远短于并发标记时间。主要用到三色标记里的增量更新算法做重新标记。
- 并发清除: 清理掉被标记为可回收的对象。这个阶段也是与用户线程并发执行的。这个阶段如果有新增对象会被标记为黑色不做任何处理
- 并发重置: 重置本次GC过程中的标记数据。确保每一次GC周期都是一个全新、干净的开始。
优势与代价
CMS三色标记与增量更新的设计,通过允许垃圾回收线程与应用线程并发工作,显著减少了垃圾收集的停顿时间,特别适合需要快速响应的B/S系统等服务端应用 。
然后,这种并发性也带来了相应的代价:
- 浮动垃圾: 在并发标记和清理阶段,用户线程仍在运行,可能会产生新的垃圾对象,这些对象在本轮GC中无法被回收,只能留待下次GC处理 。
- 内存碎片: CMS基于"标记-清除"算法,清理后会产生内存碎片。碎片过多时可能触发Full GC,而CMS的Full GC是单线程的Serial Old,可能导致长时间的停顿。
- 我们在并发执行阶段,可能应用线程产生了大对象,直接丢向了老年代,但是本身还未进行垃圾清除,所以会导致空间不够用(Concurrent Mode Failure),这时候会发生full GC,采用serial old进行垃圾回收。
- 对CPU资源敏感: 并发阶段会占用一部分CPU资源,可能影响应用吞吐量。
三色标记:
CMS收集器的三色标记原理,核心在于通过白、灰、黑三色抽象对象状态,支持标记过程与用户线程并发执行,并以增量更新策略解决并发带来的漏标问题,从而实现其低停顿的设计目标。
- 三种颜色:
- 白色: 未被访问或标记结束后仍为白色,视为垃圾
- 灰色: 已经被访问,但其引用的子对象为完全扫描
- 黑色: 自身及所有子对象均已扫描完成,确认为存活
可能这么说还是不了解其原理,我们通过下面的例子解释:
前置场景介绍:普查家族存活成员
想象你是一名 census taker(人口普查员),任务是确定一个古老家族中哪些成员还活着,哪些已经逝去(可以被遗忘了)。家族关系错综复杂。
- 整个家族 = Java堆中的对象图
- 每个家族成员 = 一个java对象
- 父子关系 = 对象引用
- 家族的始祖(未亡) = GC Roots(如静态变量、当前现场的局部变量等)
- 你的普查过程 = 垃圾回收器的标记过程
三色说明:
- 白色: 初始状态。表示**“尚未检查”**此人或普查结束是,所有贴白色标签的人将视为已故(垃圾)。
- 灰色: 表示此人确认在世,但我还没排查他的所有后代。他是你的待办事项。
- 黑色: 表示此人确认在世,并且他的所有直系后代我都排查完了。他是已完成的事项。
核心规则:绝不能将一个黑色成员的后代错误的标记为已故。
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;
以上是垃圾回收算法,以及常规的垃圾回收器,这里所说的常规是按照堆的布局来说的(新生代(伊甸区,S0,S1),老年代),下节内容我们讲解G1和zgc。