并发标记与三色标记
- 并发垃圾回收器中为了应对并发场景需要用到三色标记算法
三色标记算法
-
把对象用三种颜色标记
- 黑色:一般代表根对象,除了根对象以外,如果这个对象以及它的子对象都被扫描了,这种也是黑色的
- 灰色:只扫到当前对象,但是它的子对象还没有被扫描到,这种是灰色
- 白色:没有扫到的对象就是白色,如果扫描完所有对象后,最终是白色的为不可达对象,即垃圾对象
-
优点
- 可以异步执行,扫描颜色的线程进行的同时,用户线程也可以运行
三色标记的问题
GC并发情况下的漏标问题
- 线程1和线程2属于垃圾回收线程,假如线程1完成了所有的标记,把所有可达的都变成黑色了,线程2还处于半完成状态,还没有把gc roots引用链上的对象全部扫描完,也就是还有一些对象是白色的,如果此时用户线程把线程2负责扫描的引用链上未扫描到的对象C连接到线程1已经扫描完的引用链上的某一个对象A连接起来了,同时去掉对象C和线程2负责扫描的引用链上对象B的连接,因为线程1已经完成对对象A的扫描,那么对象C就不会再扫描,也就被漏标了
CMS中的解决方案
- Incremental Update算法
- 发现一个白色对象被一个黑色对象的时候,就把这个黑色对象重置为黑色,垃圾回收器就会这条链路没有扫完,就会再扫一次,这样白色对象就被修正为黑色
- 这种算法关注的是引用的增加
G1中的解决方案
- STAB算法(snapshot at the beginning)
- 每次扫描的时候都会进行一个快照,如果发现对象B和对象C(对象B是对象C的父对象)的连接消失了,就要把快照信息推送到GC的堆和栈,这就意味着之前的关系和引用仍然存在,再根据快照信息,就会发现对象C漏掉了,就会再扫一次
- 把两张快照叠在一起,对模子一样,一下找到变化的地方,针对这个变化的地方做处理,发现有一个连接消失了
- 这种算法关注的是引用的删除,如果这个引用删除了,并不是意味着对象C就是垃圾
解决方案对比
- CMS关注的是引用的增加,发现引用增加了就会去重新扫描,需要从头开始扫
- G1关注的是引用的删除,相比CMS要快,不用从头开始,但是快照信息也占一点内存
G1中的技术细节
G1的内存区域不固定
- 某个区域是eden区,之后可能通过标记的方式直接改成old区或者survivor区
##跨代引用
-
无论G1,还是其他传统的垃圾回收器,都会存在老年代的对象引用了新生代的对象,这种引用是跨代的
-
如果要进行新生代的单独回收?
- 这种老年代引用新生代的情况,如果JVM单独对新生代进行垃圾回收,不管这个老年对象是什么,它就会认为这个老年代的对象是gc roots
- 此时就需要对整个老年代里面的对象进行扫描,找出跟这些老年代对象关联的新生代对象
-
如何避免每一次进行young gc或者minor gc时,扫描整个老年代?
-
两种方式
1.跨代引用,通过记忆集记录,类似于hashmap的结构,key值来自于卡表数组索引,value是哪个引用,对应于下面的例子就是Rset(1,x)
2.每一个eden块都要存放一个Rset,而region只有1M,而Rset会占据很大的内存空间,对于cms而言是一整个区域,只要一个Rset就行了,所以不推荐堆空间比较小的情况下使用g1垃圾回收器
-
Rset与CardTable
-
Rset:记忆集,记录跨代引用的集合
- 对于跨代引用,是很容易辨别的,引用地址的开始和结束相差很大,可以快速扫描出来,并放到Rset里面,如果只是单独的记录x引用(c通过x引用指向d),还是意味着整个老年代都要去看
-
CardTable:卡表,如果没有卡表,不知道跨代引用的位置,就需要扫整个老年代,所以要把记忆集和卡表结合使用
- 就是一个数组card[x] = {0或者1},数组元素要么是0要么是1,0代表没有跨代引用,1代表有
- 假设使用的卡页是2的10次幂(与g1中的region大小对应),即1M,内存的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x00000x03FF(03FF是1023)、0x04000x07FF、0x0800~0x011FF的卡页内存
- 假设记忆集中的x引用关联的老年代对象在card[1]中,即card[0]=0、card[1]=1、card[2]=0
- 垃圾回收器在扫描的时候,首先看卡表,只有card[1]有跨代引用,就不需要去其他的区域扫描
安全点与安全区域–与stop the world相关
-
JVM在具体实现上,stop the world并不是强制把用户线程停止,是采用的主动式中断的方式,假如GC开始工作需要stop the word,首先设置一个标志S,用户线程会轮询这个标志位,如果标志位从0变成1,必须跑到最近的一个安全点上挂起,方便垃圾回收
-
安全点:方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点
- 安全点的设置一定要跟引用的变化有关系,引用变化就会不安全
- 但是如果线程本来就挂起了,或者阻塞了,根本没有去轮询这个标志位,就需要引入另一个概念安全区域
-
安全区域:JVM引进的一个概念,假设某一段代码有10行,发现从第4行到第7行引用关系不会发生变化(JVM根据字节码的变化来确定),这块区域就称为安全区域,如果此时用户线程正在执行这块区域上的代码,gc线程是不会管的
- GC线程就不再管已经表示在安全区域的线程,不管它是否挂起
- 当线程即将离开安全区域,需要坚持虚拟机是否完成了根节点的枚举,这里必然是要stop the world的,也就是说如果垃圾回收器完成了那些需要暂停的阶段(需要stop the world的部分,比如根节点的枚举判断、标记整理的时候),用户线程才可以继续往下面执行,如果没有完成,用户线程就一直等到完成才可以离开安全区域
低延迟的垃圾回收器
垃圾回收器三项指标
-
①内存占用
- 堆内存到底占用多少
-
②吞吐量
- 垃圾回收要吞吐量高
-
③延迟
- stop the world的时间
-
传统的垃圾回收器–cms和g1,不可能满足三者都最优,如果内存越来越高或者吞吐量越来越大,就会造成延迟变高,如果要求延迟比较低,相应的内存和吞吐量就不能太大
-
目前主流的垃圾回收器都在往延迟方向走,因为内存占用是必然的,现在很多服务器都是TB级了,吞吐量也会越来越高,延迟是一个很关键的点
Eplison
-
“不干活”的垃圾回收器
-
调测内存布局、解释器、编译器的接口,不会发生垃圾回收
ZGC
- 类似于G1的region,但是没有分代
- 延迟可以控制在10ms
- 染色指针技术-----ColoedPointers(要求内存不能超过4TB)、垃圾回收效率高
- 短暂的STW也只与GC roots大小相关(gc roots的数量相关)而与堆空间内存大小无关,哪怕内存很大,延迟也只有10ms
Shenandoah
- 非oracle公司开发的垃圾回收器
- 延迟在100ms,也是用的染色指针技术
GC日志详解
-
package ex4; import java.util.LinkedList; import java.util.List; /** * VM参数: * -XX:+PrintGCDetails -XX:+UseSerialGC * -XX:+PrintGCDetails * -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC * -XX:+PrintGCDetails -XX:+UseG1GC */ public class TestGC { /*不停往list中填充数据*/ //就使用不断的填充 堆 -- 触发GC public static class FillListThread extends Thread{ List<Object> list = new LinkedList<>(); @Override public void run() { try { while(true){ if(list.size()*512/1024/1024>=990){ list.clear(); System.out.println("list is clear"); } byte[] bl; for(int i=0;i<100;i++){ bl = new byte[512]; list.add(bl); } Thread.sleep(1); } } catch (Exception e) { } } } /*每100ms定时打印*/ public static class TimerThread extends Thread{ public final static long startTime = System.currentTimeMillis(); @Override public void run() { try { while(true){ long t = System.currentTimeMillis()-startTime; // System.out.println(t/1000+"."+t%1000); Thread.sleep(100); //0.1s } } catch (Exception e) { } } } public static void main(String[] args) { //填充对象线程和打印线程同时启动 FillListThread myThread = new FillListThread(); //造成GC,造成STW TimerThread timerThread = new TimerThread(); //时间打印线程 myThread.start(); timerThread.start(); } }
专业名字解释
-
[GC (Allocation Failure) [ParNew: 69376K->8640K(78016K), 0.0688117 secs] 69376K->62616K(251456K), 0.0762846 secs] [Times: user=0.47 sys=0.00, real=0.08 secs]
- GC:YGC
- Allocation Failure:GC原因
- ParNew: 69376K->8640K(78016K):新生代:回收前新生代大小->回收后新生代大小(新生代总大小)
- 69376K->62616K(251456K), 0.0762846 secs:回收前堆空间大小->回收后堆空间大小(总堆空间)
- Times: user=0.47 sys=0.00, real=0.08 secs:用户time 内核time 总计time
-
标识了secs的是秒,没有标识的默认是毫秒
GC常用参数
- -Xmn -Xms -Xmx –Xss 年轻代 最小堆 最大堆 栈空间
- -XX:+UseTLAB 使用 TLAB(本地线程缓冲),默认打开
- -XX:+PrintTLAB 打印 TLAB 的使用情况
- -XX:TLABSize 设置 TLAB 大小(默认是eden区的百分之一)
- -XX:+DisableExplicitGC 启用用于禁用对的调用处理的选项 System.gc(),禁用System.gc()命令
- -XX:+PrintGC 查看 GC 基本信息
- -XX:+PrintGCDetails 查看 GC 详细信息
- -XX:+PrintHeapAtGC 每次一次 GC 后,都打印堆信息
- -XX:+PrintGCTimeStamps 启用在每个 GC 上打印时间戳的功能
- -XX:+PrintGCApplicationConcurrentTime 打印应用程序时间(低)
- -XX:+PrintGCApplicationStoppedTime 打印暂停时长(低)
- -XX:+PrintReferenceGC 记录回收了多少种不同引用类型的引用(重要性低)
- -verbose:class 类加载详细过程
- -XX:+PrintVMOptions 可在程序运行时,打印虚拟机接受到的命令行显示参数
- -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 打印所有的 JVM 参数、查看所有 JVM 参数启动的初始值(必须会用)
- -XX:MaxTenuringThreshold 升代年龄,最大值 15, 并行(吞吐量)收集器的默认值为 15,而 CMS 收集器的默认值为 6。
Parallel 常用参数
- -XX:SurvivorRatio 设置伊甸园空间大小与幸存者空间大小之间的比率。默认情况下,此选项设置为 8
- -XX:PreTenureSizeThreshold 大对象到底多大,大于这个值的参数直接在老年代分配
- -XX:MaxTenuringThreshold 升代年龄,最大值 15, 并行(吞吐量)收集器的默认值为 15,而 CMS 收集器的默认值为 6。
- -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于 CMS,一般设为和 CPU 核数相同
- -XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
CMS 常用参数
- -XX:+UseConcMarkSweepGC 启用 CMS 垃圾回收器
- -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于 CMS,一般设为和 CPU 核数相同
- -XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始 CMS 收集,默认是 68%(近似值),如果频繁发生 SerialOld 卡顿,应该调小,(频繁 CMS 回
收) - -XX:+UseCMSCompactAtFullCollection 在 FGC 时进行压缩
- -XX:CMSFullGCsBeforeCompaction 多少次 FGC 之后进行压缩
- -XX:+CMSClassUnloadingEnabled 使用并发标记扫描(CMS)垃圾收集器时,启用类卸载。默认情况下启用此选项。
- -XX:CMSInitiatingPermOccupancyFraction 达到什么比例时进行 Perm 回收,JDK 8 中不推荐使用此选项,不能替代。
- -XX:GCTimeRatio 设置 GC 时间占用程序运行时间的百分比(不推荐使用)
- -XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC 会尝试用各种手段达到这个时间,比如减小年轻代
G1 常用参数
- -XX:+UseG1GC 启用 CMS 垃圾收集器
- -XX:MaxGCPauseMillis 设置最大 GC 暂停时间的目标(以毫秒为单位)。这是一个软目标,并且 JVM 将尽最大的努力(G1 会尝试调整 Young 区的块数来)来实
现它。默认情况下,没有最大暂停时间值。 - -XX:GCPauseIntervalMillis GC 的间隔时间
- -XX:+G1HeapRegionSize 分区大小,建议逐渐增大该值,1 2 4 8 16 32。随着 size 增加,垃圾的存活时间更长,GC 间隔更长,但每次 GC 的时间也会更长
- -XX:G1NewSizePercent 新生代最小比例,默认为 5%
- -XX:G1MaxNewSizePercent 新生代最大比例,默认为 60%
- -XX:GCTimeRatioGC 时间建议比例,G1 会根据这个值调整堆空间
- -XX:ConcGCThreads 线程数量
- -XX:InitiatingHeapOccupancyPercent 启动 G1 的堆空间占用比例,根据整个堆的占用而触发并发 GC 周期