上一节:《JVM 之 GC 算法》 知道 GC 算法的理论基础, 我们来看看具体的实现. 只有落地的理论, 才是真理.
一, JVM 垃圾回收器的结构
JVM 虚拟机规范对垃圾收集器应该如何实现没有规定, 因为没有最好的垃圾收集器, 只有最适合的场景.
图中展示了 7 种作用于不同分代的收集器, 如果两个收集器之间存在连线, 则说明它们可以搭配使用. 虚拟机所处的区域则表示它是属于新生代还是老年代收集器.
7 种: serial 收集器, parnew 收集器, parallel scavenge 收集器, serial old 收集器, parallel old 收集器, cms 收集器, g1 收集器(整堆收集器),
串行收集: 单垃圾收集线程, 进行收集工作, 用户进程需要等待
并行收集: 工作原理与串行一样, 只是在收集垃圾时是多条线程同时进行, 收集的效率在一般情况下自然高于单线程.
并发收集: 指用户线程与垃圾收集线程同时工作(并发: 同一时间间隔). 用户程序在继续运行, 而垃圾收集程序运行在另一个 CPU 上.
吞吐量: 吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))
1,Serial 收集器
Serial(串行)收集器: 最基本, 最古老的收集器, 只有一个线程进行垃圾收集器的工作, 并且在进行垃圾收集工作时需要暂停其他工作线程(stop the Word), 直到他工作结束;
Serial 收集器简单高效, 工作时没有线程交互的开销, 所以可以获得很高的单线程收集效率, 对于运行在 Client 模式下的虚拟机来说很适合.
"-XX:+UseSerialGC": 添加该参数来显式的使用 Serial 垃圾收集器.
2,Serial Old 收集器
Serial Old 收集器是 Seria 收集器的老年代版本, 他同样是一个单线程收集器, 使用 "标记 - 整理" 算法.
Serial Old 收集器主要用于 Client 模式下的虚拟机使用.
Server 模式下的两大用途:
在 JDK1.5 及之前的版本与 Parallel Scavenge 收集器搭配使用;
作为 CMS 收集器的后备方案, 在并发收集发生 Conturrent Mode Failure 时使用.
3,ParNew 收集器
ParNew(并行)收集器就是 Serial 收集器的多线程版本, 除了在收集垃圾时是启用多线程并行执行, 其他行为 (控制参数, 收集算法, 回收策略 / Stop The Word, 对象分配规则) 完全一样
应用场景: ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器, 因为它是除了 Serial 收集器外, 唯一一个能与 CMS 收集器配合工作的.
"-XX:+UseConcMarkSweepGC": 指定使用 CMS 后, 会默认使用 ParNew 作为新生代收集器.
"-XX:+UseParNewGC": 强制指定使用 ParNew.
"-XX:ParallelGCThreads": 指定垃圾收集的线程数量, ParNew 默认开启的收集线程与 CPU 的数量相同.
4,Parallel Scavenge 收集器
Parallel Scavenge 收集器 类似于 ParNew 收集器, Parallel Scavenge 收集器 更加关注吞吐量(高效的 CPU 利用率).CMS 等垃圾收集器关注更多的是用户线程的停顿时间(提搞用户体验);Parallel Scavenge 收集器提供很多参数供我们找到最合适的停顿时间或者最大吞吐量. JDK1.8 默认的方式;
Parallel Scavenge 收集器提供了两个参数来用于精确控制吞吐量, 一是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数, 二是控制吞吐量大小的 -XX:GCTimeRatio 参数;
"-XX:MaxGCPauseMillis" 参数允许的值是一个大于 0 的毫秒数, 收集器将尽可能的保证内存垃圾回收花费的时间不超过设定的值(但是, 并不是越小越好, GC 停顿时间缩短是以牺牲吞吐量和新生代空间来换取的, 如果设置的值太小, 将会导致频繁 GC, 这样虽然 GC 停顿时间下来了, 但是吞吐量也下来了).
"-XX:GCTimeRatio" 参数的值是一个大于 0 且小于 100 的整数, 也就是垃圾收集时间占总时间的比率, 默认值是 99, 就是允许最大 1%(即 1/(1+99))的垃圾收集时间.
"-XX:UseAdaptiveSizePolicy" 参数是一个开发, 如果这个参数打开之后, 虚拟机会根据当前系统运行情况收集监控信息, 动态调整新生代的比例, 老年大大小等细节参数, 以提供最合适的停顿时间或最大的吞吐量, 这种调节方式称为 GC 自适应的调节策略.
应用场景: 注重高吞吐量以及 CPU 资源敏感的场合, 都可以优先考虑 Parallel Scavenge+Parallel Old 收集器.
5,Paraller Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本, 使用多线程和 "标记 - 整理" 算法.
在 JDK1.6 中才出现.
6,CMS(Conturrent Mark Sweep)收集器
CMS 收集器是一种以获取最短回收停顿时间为目标的收集器. CMS 收集器是基于 "标记 - 清除" 算法实现, 它的整个运行过程可以分为:
初始标记: 标记一下 GC Roots 能直接关联到的对象, 这个过程速度很快, 但是会暂停其他用户线程(Stop the Word)
并发标记: 进行 GCRoots Tracing 的过程, 同时开启 GC 和用户线程, 用一个闭包的结构去记录可达对象, 但是在这个阶段结束, 该闭包不能保证其包含当前所有的可达对象. 因为用户进程可能会不断的更新引用域, 所以 GC 线程无法保证可达性分析的实时性. 所以这个算法会跟踪记录这些发生引用更新的地方.
重新标记: 修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录, 该阶段会 GC 停顿, 停顿时间比初始标记时间稍长, 单远比并发标记时间短.
并发清除: 开启用户线程, 同事 GC 线程清除死亡的对象
CMS 收集器运行的整个过程中, 最耗费时间的是并发标记和并发清除, GC 收集器线程和用户线程是一起工作的, 所以总体来说, CMS 收集器的内存回收过程是与用户线程一起并发执行的.
优点: 并发收集, 低停顿.
缺点:
1,CMS 收集器对 CPU 资源非常敏感. 虽然在两个并发阶段不会导致用户线程停顿, 但是会因为占用了一部分线程而导致应用程序变慢, 总吞吐量下降. CMS 默认启动的回收线程数是(CPU 数量 + 3)/4.
2,:CMS 收集器无法处理浮动垃圾, 可能出现 "Conturrent Mode Failure" 失败而导致另一次 Full GC 产生. 由于 CMS 并发清除阶段用户线程还在运行, 伴随着程序还在产生新的垃圾, 这一部分垃圾出现在标记之后, CMS 无法在当次收集中处理掉它们, 只能留到下次再清理, 这一部分垃圾称为 "浮动垃圾". 也正是由于在垃圾收集阶段用户线程还在运行, 那么也就需要预留有足够的内存空间给用户线程使用, 因此 CMS 收集器不能像其他收集器那样等待老年代填满之后再进行收集, 需要预留一部分空间给并发收集时用户程序使用. 可以通过 "-XX:CMSInitiatingOccupancyFraction" 参数设置老年代内存使用达到多少时启动收集.
3,: 由于 CMS 收集器是一个基于 "标记 - 清除" 算法的收集器, 那么意味着收集结束会产生大量碎片, 有时候往往还有很多内存未使用, 但是没有一块连续的空间来分配这个大对象, 导致不得不提前触发一次 Full GC.CMS 收集器提供了一个 "-XX:UseCMSCompactAtFullCollection" 参数 (默认是开启的) 用于在 CMS 收集器顶不住要 FullGC 时开启内存碎片整理(内存碎片整理意味着无法并发执行不得不停顿用户线程). 参数 "-XX:CMSFullGCsBeforeCompaction" 来设置执行多少次不压缩的 Full GC 后, 跟着来一次带压缩的(默认值是 0, 意味着每次进入 Full GC 时都进行碎片整理).
7,G1(Garbage-First)收集器
G1 的内存模型
G1 收集器没有新生代和老年代的概念, 而是将 Java 堆划分为一块块独立的大小相等的 Region. 当要进行垃圾收集时, 首先估计每个 Region 中的垃圾数量, 每次都从垃圾回收价值最大的 Region 开始回收, 因此可以获得最大的回收效率
Humongous 是特殊的 Old 类型, 专门放置大型对象. 这样的划分方式意味着不需要一个连续的内存空间管理对象. G1 将空间分为多个区域, 优先回收垃圾最多的区域.
G1 采用的是 Mark-Copy , 有非常好的空间整合能力, 不会产生大量的空间碎片
G1 的一大优势在于可预测的停顿时间, 能够尽可能快地在指定时间内完成垃圾回收任务, 在 JDK11 中, 已经将 G1 设为默认垃圾回收器, 通过 jstat 命令可以查看垃圾回收情况, 在 YGC 时 S0/S1 并不会交换.
一个对象和它内部所引用的对象可能不在同一个 Region 中, 那么当垃圾回收时, 是否需要扫描整个堆内存才能完整地进行一次可达性分析?
当然不是, 每个 Region 都有一个 Remembered Set, 用于记录本区域中所有对象引用的对象所在的区域, 从而在进行可达性分析时, 只要在 GC Roots 中再加上 Remembered Set 即可防止对所有堆内存的遍历.
回收步骤
初始标记: 标记与 GC Roots 直接关联的对象, 停止所有用户线程, 只启动一条初始标记线程, 这个过程很快.
并发标记: 进行全面的可达性分析, 开启一条并发标记线程与用户线程并行执行. 这个过程比较长.
最终标记: 标记出并发标记过程中用户线程新产生的垃圾. 停止所有用户线程, 并使用多条最终标记线程并行执行.
筛选回收: 回收废弃的对象. 此时也需要停止一切用户线程, 并使用多条筛选回收线程并行执行.
G1 为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个 Java 堆中进行全区域的垃圾收集. G1 跟踪各个 Region 里面的垃圾堆积的大小, 在后台维护一个优先列表, 每次根据允许的收集时间, 优先回收价值最大的 Region. 这样就保证了在有限的时间内可以获取尽可能高的收集效率.
G1 与其他收集器的区别?
其他收集器的工作范围是整个新生代或者老年代, G1 收集器的工作范围是整个 Java 堆. 在使用 G1 收集器时, 它将整个 Java 堆划分为多个大小相等的独立区域 (Region). 虽然也保留了新生代, 老年代的概念, 但新生代和老年代不再是相互隔离的, 他们都是一部分 Region(不需要连续) 的集合.
二, 如何选择垃圾收集器
1, 单 CPU 或者小内存, 单机程序 - -XX:+UseSerialGC
2, 多 CPU, 需要大吞吐量, 如后台计算型应用, 允许工作线程停顿超过 1 秒 -XX:+UseParallelGC + -XX:+UseParallelOldGC
3, 多 CPU, 追求低停顿时间, 快速响应如互联网应用 -XX:+UseParNewGC + -XX:+UseConcMarkSweepGC
4,JVM 自己选择
5, 官方推荐 G1, 高性能
来源: https://www.cnblogs.com/jalja365/p/12182064.html