【JVM原理】垃圾回收机制(3)--垃圾收集器
前言
Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)
JVM内存结构
JVM类加载机制
JVM内存溢出分析
HotSpot对象创建、内存、访问
JVM垃圾回收机制(1)—如何判定对象可以回收
JVM垃圾回收机制(2)—垃圾收集算法
JVM垃圾回收机制(3)—垃圾收集器
JVM垃圾回收机制(4)—内存分配和回收策略
一 垃圾收集器概述
垃圾收集器是垃圾收集算法(标记-清除算法、复制算法、标记-整理算法)的具体实现。
Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、
不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参
数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
这里主要讨论 HotSpot 虚拟机中的垃圾收集器。
1、垃圾收集器组合
JDK7/8后HotSpot虚拟机中的垃圾收集器和组合搭配示意图:
图中表明总的有七种收集器,Serial、ParNew、Parallel Scavenge、Serial Old、
Parallel Old、CMS、G1;
新生代收集器:Serial、ParNew、Parallel Scavenge。
老年代收集器:Serial Old、Parallel Old、CMS。
整理收集器:G1。
这些收集器很多是可以组合使用的,图中连线的表示可以搭配使用。
2、并发和并行垃圾收集的区别
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如 ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个 CPU 上;
如 CMS、G1(也有并行);
3、Minor GC和Full GC的区别
Minor GC 又称为新生代 GC,是指发生在新生代的垃圾收集动作。
因为大多数 Java 对象都是”朝生夕灭”的,所以 Minor GC 回收非常的频繁,一般回收速度
也非常快。
Full GC又称为 Major GC 或老年代 GC,是指发生在老年代的垃圾收集动作。
出现 Full GC 经常会伴随至少一次的 Minor GC(不是绝对,Parallel Sacvenge 收集器就
可以选择设置 Major GC 策略);
Major GC 速度一般比 Minor GC 慢 10 倍以上;
接下来主要分析各个收集器的特性、基本原理和使用场景。
二 Serial收集器
Serial (串行)收集器是最基本、发展历史最悠久的收集器,在 JDK1.3.1 之前是虚拟机新生代
收集的唯一选择。
1、特性
Serial 收集器主要有以下特性:
1)针对新生代进行回收。
2)采用复制算法。
3)单线程收集。
4)回收时必须停止其他所有工作线程,直到收集结束。即”Stop The Word”,给用户的体验不好。
2、基本原理
Serial/Serial Old 收集器的运行过程示意图:
采用单线程进行收集,收集时停止所有用户工作线程,直到收集完成,收集完成才又启动
用户线程。
3、使用场景
Serial 收集器依然是 HotSpot 在 Client 模式下默认的新生代收集器;
也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);对于限定单个 CPU
的环境来说,Serial 收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
在用户的桌面应用场景中,可用内存一般不大(几十 M 至一两百 M),可以在较短时间
内完成垃圾收集(几十 MS 至一百多 MS),只要不频繁发生,这是可以接受的。
4、设置参数
“-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器;
三 ParNew收集器
ParNew 收集器是 Serial 收集器的多线程版本。
1、特性
1)多线程收集;
2)除多线程外,其余特性与 Serial(串行)收集器一样,比如控制参数、收集算法
(都用复制算法)、Stop The World(收集时停止所有用户线程)、对象分配规则、
回收策略等都与 Serial 收集器完全一样。ParNew 和 Serial 实现代码很多都是共用的,
可以看成是对 Serial 收集效率提升的优化版本。
2、基本原理
ParNew 收集器工作过程示意图:
采用多线程收集,收集时也没逃过停止所有用户线程的宿命,只是用多线程快些,
用户感知的 GC 停顿小些,用户体验好些。
3、应用场景
在 Server 模式下,ParNew 收集器是一个非常重要的收集器,因为除 Serial 外,目前只有它能
与 CMS 收集器配合工作;CMS 下面会介绍,它是 HotSpot 在 JDK1.5 推出的第一款真正
意义上的并发(Concurrent)收集器,它第一次实现了让垃圾线程和用户线程同时工作。
CMS 作为老年代收集器,但却无法与 JDK1.4 已经存在的新生代收集器 Parallel Scavenge
配合工作;因为 Parallel Scavenge(以及G1)都没有使用传统的 GC 收集器代码框架,
而另外独立实现;而其余几种收集器则共用了部分的框架代码;
同时,在单个 CPU 环境中,ParNew 收集器不会比 Serail 收集器有更好的效果,因为存在
线程交互开销,这是多线程避免不了的线程上下文切换开销,但是在多个 CPU 下,
Serial 跟 ParNew 没法相比,现在是多核时代,”榨干” CPU 来提高运行效率是程序追求的,
所以 ParNew 配合 CMS 成为不二选择。
4、参数设置
“-XX:+UseConcMarkSweepGC”:指定使用 CMS 后,会默认使用 ParNew 作为新生代收集器;
“-XX:+UseParNewGC”:强制指定使用 ParNew;
“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew 默认开启的收集线程与 CPU
的数量相同;
四 Parallel Scavenge收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是用复制算法、同时也是多线程收集器,
看上去和 ParNew 一样,但是它关注的点与其他收集器不同,别的收集器主要关心如何
缩短 GC 停顿时间,而 Parallel Scavenge 关心的是吞吐量,所以 Parallel Scavenge
也称为”吞吐量优先”收集器。
1、特性
1)有些特点与 ParNew 相似,比如针对新生代收集,采用复制算法,多线程收集。
2)别的收集器主要关注 GC 停顿时间,而 Parallel Scavenge 主要目标是达到一个
可控制的吞吐量。
2、应用场景
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,
而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在
后台运算而不需要太多交互的任务。例如:长时间处理大数据,科学计算等等。
3、基本原理
Parallel Scavenge/Parallel Old 收集器工作过程示意图:
4、参数设置
Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集
停顿时间的“-XX:MaxGCPauseMillis”参数以及直接设置吞吐量大小的“-XX:GCTimeRatio”参数。
“-XX:MaxGCPauseMillis” 控制最大垃圾收集停顿时间参数允许的值是一个大于 0 的毫秒数,
收集器将尽可能地保证内存回收花费的时间不超过设定值;
MaxGCPauseMillis 设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,
因为可能导致垃圾收集发生得更频繁;
“-XX:GCTimeRatio” 设置垃圾收集时间占总时间的比率,该值范围为 0<n<100 的整数;
GCTimeRatio 相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n)
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%—1/(1+19);
默认值是1%—1/(1+99),即n=99;
垃圾收集所花费的时间是年轻一代和老年代收集的总时间;
如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;
还有一个参数,”-XX:+UseAdptiveSizePolicy”
开启这个参数后,就不用手工指定一些细节参数,如:
新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
JVM 会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的
停顿时间或最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomiscs);
当你不知道怎么优化的时候,自适应调节策略是一种值得推荐的方式:
只需设置好内存数据大小(如”-Xmx”设置最大堆);
然后使用 “-XX:MaxGCPauseMillis” 或 “-XX:GCTimeRatio” 给 JVM 设置一个优化目标;
那些具体细节参数的调节就由 JVM 自适应完成;
这也是 Parallel Scavenge 收集器与 ParNew 收集器一个重要区别;
五 Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用”标记-整理”算法。
1、特性
1)针对老年代进行收集;
2)采用”标记-整理”算法;
3)是一个单线程收集器;
2、基本原理
Serial/Serial Old 收集器运行示意图:
3、应用场景
主要用于 Client 模式;
而在 Server 模式有两大用途:
1)在 JDK1.5 及之前,与 Parallel Scavenge 收集器搭配使用(JDK1.6有 Parallel Old 收集器
可搭配);
2)作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用;
六 Parallel Old 收集器
Parallel Old 是 Parallel Scagenge 收集器的老年代版本,使用多线程和”标记-整理”算法。
1、特性
1)针对老年代收集;
2)”标记-整理”算法;
3)多线程收集;
2、基本原理
Parallel Scavenge/Parallel Old 收集器工作过程示意图:
3、应用场景
Parallel Old 收集器是在 JDK1.6 中才开始提供的,用于替代老年代 Serial Old 收集器。
因为 Serial Old 收集器在服务端应用性能上是一个”拖累”。
特别是在 Server 模式,多 CPU 的情况下;这样在注重吞吐量以及 CPU 资源敏感的场景,
就有了 Parallel Scavenge 加 Parallel Old 收集器的”给力”应用组合;
4、参数设置
“-XX:+UseParallelOldGC”:指定使用 Parallel Old 收集器;
七 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以 获取最短回收停顿时间为目的的收集器,
也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;
1、特性
1)正对老年代收集;
2)采用”标记-清除”算法;
3)以获取最短回收停顿时间为目标;
4)并发收集、低停顿;
5)需要更多的内存;
2、基本原理
CMS 收集器收集过程分为四个步骤:
1)初始化标记
仅标记一下 GC Roots 能直接关联到的对象;
速度很快;
但需要”Stop The World”;
2)并发标记
进行 GC Roots Tracing 的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
3)重新标记
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
4)并发清除标记
回收所有的垃圾对象;
其中初始标记和重新标记这两个步骤仍然需要”Stop The Word”。整个过程中耗时最长的
并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS 收集器的内存
回收过程与用户线程一起并发执行;
CMS 收集器运行示意图:
3、CMS 三个缺点
1)CMS 收集器对 CPU 资源非常敏感。
并发收集虽然不会暂停用户线程,但因为占用一部分 CPU 资源,还是会导致应用程序变慢,
总吞吐量降低。CMS 的默认收集线程数量是=(CPU数量+3)/4;当 CPU 数量多于 4 个,
收集线程占用的 CPU 资源多于 25%,对用户程序影响可能较大;不足 4 个时,影响更大,
可能无法接受。
2)CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “Concurrent Mode Failure”失
败而导致一次 Full GC 的产生。
浮动垃圾(Floating Garbage)
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的
内存空间,不能像其他收集器在老年代几乎填满再进行收集;也要可以认为 CMS 所需要的
空间比其他垃圾收集器大;
“-XX:CMSInitiatingOccupancyFraction”:设置 CMS 预留内存空间;
JDK1.5 默认值为 68%;JDK1.6 变为大约 92%;
“Concurrent Mode Failure”失败
如果 CMS 预留内存空间无法满足程序需要,就会出现一次”Concurrent Mode Failure”失败;
这时JVM启用后备预案:临时启用 Serail Old 收集器,而导致另一次 Full GC 的产生;
这样的代价是很大的,所以 CMSInitiatingOccupancyFraction 不能设置得太大。
3)产生大量的内存碎片
由于 CMS 基于”标记-清除”算法,清除后不进行压缩操作;
产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,
从而需要提前触发另一次 Full GC 动作。
内存碎片解决方法:
“-XX:+UseCMSCompactAtFullCollection”
使得 CMS 出现上面这种情况时不进行 Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
“-XX:+CMSFullGCsBeforeCompaction”
设置执行多少次不压缩的 Full GC 后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为 0,也就是说每次都执行 Full GC,不会进行压缩整理;
由于空间不再连续,CMS 需要使用可用”空闲列表”内存分配方式,这比简单实用”碰撞指针”
分配内存消耗大;
八 G1收集器
G1(Garbage-First)是面向服务端应用,JDK7-u4 才推出商用的收集器,是当今收集器
技术发展的最前沿成果之一;
1、特性
1)并行与并发
能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU或CPU核心)来
缩短”Stop The World”停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
2)分代收集,收集范围包括新生代和老年代
能独立管理整个 GC 堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象以获得更好的收集效果;
虽然保留分代概念,但 Java 堆的内存布局有很大差别;将整个堆划分为多个大小相等的
独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分 Region(不需要连续)的集合;
3)空间整合
从整体看,是基于标记-整理算法实现的收集器,从局部(两个Region间)看,
是基于”复制”算法实现的;
运作期间都不会产生内存碎片,收集后能提供规整的可用内存,有利于长时间运行,
分配大对象是不会因为无法找到连续内存空间而提前触发一下GC;
4)可预测的停顿:低停顿的同时实现高吞吐量
G1 除了追求低停顿处,还能建立可预测的停顿时间模型,可以明确指定 M 毫秒时间片内,
垃圾收集消耗的时间不超过 N 毫秒;
2、基本原理
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
1)初始标记(Initial Marking)
仅标记一下 GC Roots 能直接关联到的对象;
且修改 TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的
Region 中创建新对象;
需要”Stop The World”,但速度很快;
2)并发标记(Concurrent Marking)
进行 GC Roots Tracing 的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行;
并不能保证可以标记出所有的存活对象;
3)最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的 Remembered Set Log;
这里把 Remembered Set Log 合并到 Remembered Set 中;
需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
4)筛选回收(Live Data Counting and Evacuation)
首先排序各个 Region 的回收价值和成本;
然后根据用户期望的 GC 停顿时间来制定回收计划;
最后按计划回收一些价值高的 Region 中垃圾对象;
回收时采用”复制”算法,从一个或多个 Region 复制存活对象到堆上的另一个空的 Region,
并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量;
G1 收集器运行示意图:
全堆收集,多种算法结合,与用户线程并行进行。
3、应用场景
面向服务端应用,针对具有大内存、多处理器的机器;
最主要的应用是为需要低 GC 延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约 6GB 或更大时,可预测的暂停时间可以低于 0.5 秒;
用来替换掉 JDK1.5 中的 CMS 收集器;
在下面的情况时,使用 G1 可能比 CMS 好:
1)超过 50% 的 Java 堆被活动数据占用;
2)对象分配频率或年代提升频率变化很大;
3)GC 停顿时间过长(长于 0.5 至 1 秒)。
是否一定采用 G1 呢?也未必:
如果现在采用的收集器没有出现问题,不用急着去选择 G1;
如果应用程序追求低停顿,可以尝试选择 G1;
是否代替 CMS 需要实际场景测试才知道。
4、参数设置
“-XX:+UseG1GC”:指定使用 G1 收集器;
“-XX:InitiatingHeapOccupancyPercent”:当整个Java堆的占用率达到参数值时,
开始并发标记阶段;默认为 45;
“-XX:MaxGCPauseMillis”:为 G1 设置暂停时间目标,默认值为 200 毫秒;
“-XX:G1HeapRegionSize”:设置每个 Region 大小,范围 1MB 到 32MB;
目标是在最小 Java 堆时可以拥有约 2048 个 Region;
到这里,HotSpot 虚拟机中收集器大概都了解了一遍。
参考文献
《深入理解Java虚拟机》 (第二版) 周志明 著;
还没有评论,来说两句吧...