总体概览:
1.吞吐量和低延迟的关系
吞吐量=用户线程的时间/(用户线程的时间+GC的时间),从公式可以看出,GC的总时间越小,吞吐量越大
低延迟:指的是每次GC的停顿时间,停顿越短,延迟越低,用户感觉就越不卡
举个例子:假如只有2个线程,垃圾收集分为2个阶段:标记阶段+清除阶段
方案1:现在这2个线程都作为GC线程去收集垃圾,这次收集完所有的垃圾需要10s,收集垃圾时需要STW也就是用户线程会暂停10s,那么用户感觉到会卡顿10s,也就是高延迟了;
方案2:我们可以让用户线程和GC线程一起运行,其中一个线程运行GC,一个线程运行用户线程,这样花费下来的话,肯定GC的总时间会大于10s(因为收集垃圾的线程少了),但用户不会感觉到卡顿10s,因为用户线程也在运行,当然,不可能每个时刻用户线程和GC线程都一起运行的,肯定有个阶段是需要STW的,我们假设标记阶段假设需要2s,清除阶段需要10s(因为此时只有一个线程收集GC,另一个运行用户线程了),那么在清除阶段用户线程也可以运行,只有标记阶段需要stw,此时用户感觉到卡顿只是2s
对比可知道,第一种方案垃圾收集的总时间相对比较少,所以它的吞吐量高,但延迟也高,这种垃圾收集器指的是(partNew,parallel,parallel old,这些并发的垃圾收集器)
方案二:吞吐量低了,但延迟越低。用户感觉更顺畅了,这种代表是CMS,G1以及未来的ZGC
2.GC算法:
复制算法:
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对
内存区间的一半进行回收。
标记清除:算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单,但是会带来
两个明显的问题:
1. 效率问题 (如果需要标记的对象太多,效率不高)
2. 空间问题(标记清除后会产生大量不连续的碎片)
标记整理:根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
3.垃圾收集器
如何判断对象是否是垃圾对象?
1.引用计算法:当一个对象被引用了,然后在计数器加1,当计数器为0时,该对象就可以被回收,但有个缺点,在相互引用的时候,会导致没法回收
2. 根搜索法也就是GCRoot,被GCRoot引用的对象都是非垃圾对象(能称为GCROOT的对象有静态变量,运行中的线程中的变量,常量池的中的变量等)
serial和serial old:(-XX:+UseSerialGC -XX:+UseSerialOldGC)单线程垃圾收集器,在以前机器性能还不高时使用该垃圾收集器,现在多核CPU的场景下,该垃圾收集器已经不适合了
parallel 和 parallel old:(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
CMS:-XX:+UseConcMarkSweepGC(old)
CMS的目标是追求最小停顿:
初始标记:需要STW,为了减少停顿时间,这里仅仅标识出GCRoot 直接因为的 对象,所以非常快
并发标记:开始沿着GCRoot对象去搜索关联引用的对象,这个时候应用线程不需要停下来,这样耗时GC的工作就跟用户线程一起运行,用户感觉不到停顿
重新标记:需要STW,由于用户线程在第二阶段也在运行,有些GCRoot已经属于垃圾了,这样会导致多标,也有些新的产生的垃圾没被标注,这是需要重新标注
并发清理:可以跟用户线程一起运行,清理未被标注的对象(垃圾对象)
并发充值:重置本次标注的数据
问题:
1.并发清除或者并发标注时,由于用户线程还在运行,可能他们产生的垃圾又触发了GC,导致concurrent mode failure
解决:遇到这种情况,会改成使用serial old垃圾收集器,导致STW
2.由于CMS使用的是标记清除的垃圾算法,会导致大量的内存碎片:
解决:-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片) -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
3.在并发标记时和并发清理时,用户线程新增的对象或者之前的对象关系变更,怎么处理,这里要用到的是三色标记法:
如:在并发清理时,新增的对象,由于没有在GCROOT可达性分析对象里,那这些新增的对象总不能当作垃圾吧,这时直接将其标注为黑色,在下一轮垃圾收集在处理即可
多标和漏标:
多标:由于用户线程在运行中,某些对象可能已经称为垃圾对象了,但已被标注为垃圾对象,这种问题不重要,在下一次垃圾回收时会被处理
漏标:如A是GCROOT对象,在并发标记清理时,由于用户线程还在运行,此时A可能又引用了C对象,但C对象没有被标注,导致漏标导致C被回收了,此时会产生严重的bug
解决方案:增量更新和原始快照(STAB)
增量更新:当黑色对象新增指向白色对象时,将奇记录起来,变成灰色对象,等重新标记时,再次扫描这些灰色对象
原始快照:在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑
色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
这两种方式都是通过写屏障实现的:
卡表和记忆集合:关系相当hashMap和Map的关系:
卡表:新生代存有老年代指向新生代的指针集合,卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
其实所有跨代的引用都会存在这种卡表;
CMS相关的一些命令
1. -XX:+UseConcMarkSweepGC:启用cms
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设
定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引
用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
partNew:因为parallel没法跟CMS搭配使用,所以弄了个partNew用于新生代,跟parallel差不多
G1垃圾收集器:
特点: 1.将内存分成了大小相同的区,也叫Region,其中红色的代表收集大对象,默认是2048个region区域
2.采用复制算法
3. 可以设置垃圾回收最大的停顿时间(-XX:MaxGCPauseMillis)
阶段:
其中初始标记,并发标记,最终标记(类似CMS的重新标记)跟cms一样,只是回收阶段不一样,筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划
安全点和安全区域:
观察G1的执行图可以看到,有个SafePoint的东西,什么意思呢?考虑一下,GC是不是在任意时刻都可以STW呢,如我在做i++运算时,底层指令是很多的总不能在执行一半时就STW了吧,因此什么时候能进行STW,这个就是安全点
安全区域:如一个线程执行了睡眠指令,GC不可能等待睡眠完再执行吧,因此睡眠前的一段代码肯能属于安全区域
安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比
如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
这些特定的安全点位置主要有以下几种:
1. 方法返回之前
2. 调用某个方法之后
3. 抛出异常的位置
4. 循环的末尾
大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程
时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和
安全点是重合的。
安全区域又是什么?
Safe Point 是对正在执行的线程设定的。
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
G1收集器参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个
年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合
收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能
就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这
个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一
会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都
是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清
理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立
即停止混合回收,意味着本次混合回收就结束了。,
什么场景适合使用G1
1. 50%以上的堆被存活对象占用
2. 对象分配和晋升的速度变化非常大
3. 垃圾回收时间特别长,超过1秒
4. 8GB以上的堆内存(建议值)
5. 停顿时间是500ms以内