参考:
https://blog.csdn.net/CrankZ/article/details/86009279
https://www.jianshu.com/p/2a1b2f17d3e4
https://www.javatt.com/p/47358
垃圾收集器
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆收集器:G1
1、Serial收集器
- JDK1.3.1前是HotSpot新生代收集的唯一选择;
- Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。
- -XX:+UseSerialGC:串联收集器
2、ParNew收集器
- ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。
- 除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作
- 除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
配置
- -XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器
- -XX:+UseParNewGC:强制指定使用ParNew;
- -XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
3、Parallel Scavenge收集器
- Parallel Scavenge收集器和ParNew类似,新生代的收集器,同样用的是复制算法,也是并行多线程收集。与ParNew最大的不同,它关注的是垃圾回收的吞吐量。
- Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。
参数
- “-XX:+MaxGCPauseMillis”:控制最大垃圾收集停顿时间,大于0的毫秒数;这个参数设置的越小,停顿时间可能会缩短,但也会导致吞吐量下降,导致垃圾收集发生得更频繁。
- “-XX:GCTimeRatio”:设置垃圾收集时间占总时间的比率,0<n<100的整数,就相当于设置吞吐量的大小。
- “-XX:+UseAdptiveSizePolicy”,开启这个参数后,就不用手工指定一些细节参数,如:
-
- 新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
4、Serial Old收集器(标记-整理算法)
- 如上图所示,Serial 收集器在新生代和老年代都有对应的版本,除了收集算法不同,两个版本并没有其他差异。
- Serial 新生代收集器采用的是复制算法。
- Serial Old 老年代采用的是标记 - 整理算法。
- 如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;另一种用途就是作为CMS收集器的后备预案,在并发收集发生"Concurrent Mode Failure"时使用。
5、Parallel Old收集器
- Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
- Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6开始提供。
6、CMS(Concurrent Mark Sweep)收集器
触发条件
- -XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发
- 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%。
- 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled。
- 新生代的晋升担保失败。
0、三色标记算法
- 白色对象,表示自身未被标记;
- 灰色对象,表示自身被标记,但内部引用未被处理;
- 黑色对象,表示自身被标记,内部引用都被处理;
1、初始标记(initial mark)STW
- 单线程执行 需要“Stop The World”
- 标记GC Roots可达的老年代对象;
- 遍历新生代对象,标记可达的老年代对象;
该阶段执行完以后,如图1,灰色标记对象。
1.1 GC Roots对象的包括如下几种:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
- 方法区中的类静态属性引用的对象 ;
- 方法区中的常量引用的对象 ;
- 本地方法栈中JNI的引用的对象;
-
- ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。
2、并发标记(concurrent mark)
- 该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
- 并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
2.1 预清理(concurrent Preclean)
- 通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:
-
- 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
-
- 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable,而是一个类似的数据结构,叫ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)
2.1 可被终止的预清理(concurrent abortable Preclean)
- 该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
- 因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
- 处理 From 和 To 区的对象,标记可达的老年代对象
- 和上一个阶段一样,扫描处理Dirty Card中的对象
当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:
- 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
- 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
- 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)
3、重新标记(remark)STW
- 在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。
- 此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,
- 可能产生新的引用关系如下:
- 老年代的新对象被GC Roots引用
- 老年代的未标记对象被新生代对象引用
- 老年代已标记的对象增加新引用指向老年代其它对象
- 新生代对象指向老年代引用被删除
处理
- 遍历新生代对象,重新标记
- 根据GC Roots,重新标记
- 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过
4、并发清除(concurrent sweep)
- 并发清除之前所标记的垃圾。
- 其他用户线程仍可以工作,不需要停顿。
参数
- -XX:+UseConcMarkSweepGC:使用CMS收集器
- -XX:+ UseCMSCompactAtFullCollection:Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
- -XX:+CMSFullGCsBeforeCompaction:设置进行几次Full GC后,进行一次碎片整理
- -XX:ParallelCMSThreads:设定CMS的线程数量(一般情况约等于可用CPU数量)
1、漏标-读写屏障
public class MarkGC2 {
private static final Class1 class1 = new Class1();
public static void main(String[] args) {
class1.class2 = null;
}
static class Class1 {
Class2 class2 = new Class2();
}
static class Class2 {
{
new Timer().schedule(new TimerTask() {
@Override
public void run() {
//重新赋值引用
class1.class2 = Class2.this;
}
}, 3000);
}
}
}
- 如上述代码示例,class1作为静态变量,而mian方法中将class1中的class2置为null,如果这时候可达性算法运行,就会扫描不到class1里面的class2这个对象,因为引用已经被置为null了。
- 当可达性算法执行结束,class2这个对象将自己的引用赋值到了这个静态变量class1里面,按理说它这时候是有被引用,不能被清除,但是可达性算法已经结束了,不会去处理这个被引用的白色对象,那么最终这个class2对象会被回收,这就是漏标。
- 漏标会导致被引用的对象被当成垃圾误删除,这是严重的问题,必须要解决掉。
- 有两种解决方案
- 增量更新(Incremental Update)
- 增量更新就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
- 可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
- 原始快照(Snapshot At The Beginning , SATB )
- 原始快照就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的是让这种对象在本轮GC清理中能存活下来,等待下一轮GC的时候重新扫描,这个对象也有可能是浮动垃圾)
- 以上,无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。
2、写屏障
所谓的写屏障,其实就是指在赋值操作前后,加入一些处理,记录下来,可以参考Spring AOP的概念。
- 写屏障实现 SATB
当对象的变量引用发生变化时,比如 class1.class2=null ,可以利用写屏障,将原来变量的引用记录下来 。 - 写屏障实现增量更新
当对象的变量引用发生变化时,比如class1.class2 = Class2.this ; 可以利用写屏障,将新的变量引用对象记录下来。
3、漏标处理方案
- CMS:写屏障 + 增量更新
- G1,Shenandoah:写屏障+SATB
- ZGC:读屏障:读屏障就是当读取成员变量时,一律记录下来
1、对CPU资源非常敏感
2、浮动垃圾(Floating Garbage)
- 在并发标记过程中,如果由于方法运行结束导致部分局部变量(GC Roots)被销毁,而这个GC Roots引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。
- 这部分本应该回收但是没有回收到的内存,被称之为"浮动垃圾"。
- 另外,针对并发标记(还有并发清理)执行后开始产生的新对象,通常的做法是直接全部标记为黑色,本轮不会进行清除。这部分对象期间也可能会变为垃圾,但是不会被清除,这也算是浮动垃圾的一部分。
3、"Concurrent Mode Failure"失败
- 这个异常发生在cms正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。
- -XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC。
-XX:+UseCMSInitiatingOccupancyOnly 如果不指定, 只是用设定的回收阈值CMSInitiatingOccupancyFraction,则JVM仅在第一次使用设定值,后续则自动调整会导致上面的那个参数不起作用。
4、产生大量内存碎片
- CMS是基于标记-清除算法的,CMS只会删除无用对象,不会对内存做压缩,会造成内存碎片,这时候我们需要用到这个参数: -XX:CMSFullGCsBeforeCompaction=n
- 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩。
5、减少remark阶段停顿
- 一般CMS的GC耗时80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:-XX:+CMSScavengeBeforeRemark。
- 在执行remark操作之前先做一次Young GC,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。
代码实验
- -XX:MaxTenuringThreshold=5
- 作用:对象晋升到老年代的阈值,默认值15,cms默认6,g1默认15,
- gc算法中会计算每个对象的年龄,,如果发现总大小已经大于了survivor的50%,那么需要调整阈值,不能等到默认的15次gc后,才晋升,否则导致survivor空间不足
/**
* -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=4194304 -XX:+UseSerialGC
-XX:MaxTenuringThreshold=5
-XX:+PrintTenuringDistribution
*/
public class Test1 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] b1 = new byte[size * 2];
byte[] b2 = new byte[size * 2];
byte[] b3 = new byte[size * 2];
byte[] b4 = new byte[size * 2];
System.out.println("end");
//[GC (Allocation Failure) [PSYoungGen: 6290K->856K(9216K)] 6290K->4960K(19456K), 0.0422960 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
//[PSYoungGen: 6290K->856K(9216K)]
//PS Parallel Scavenge多线程收集器
//6290K->856K = 5434k 执行gc后新生代释放的空间
//9216K 新生代大小,-Xmn10m,8:1:1 即有1m的空间浪费掉了
//6290K->4960K(19456K)
//6290K->4960K =1330k 执行gc后总的空间释放的容量
//19456K 总空间大小19m
//ParOldGen:Parallel Old收集器
}
}
java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
/**
* -verbose:gc -Xmx200m -Xms200m -Xmn50m -XX:+PrintGCDetails -XX:+PrintGCDateStamps
* -XX:TargetSurvivorRatio=60
* -XX:MaxTenuringThreshold=3 -XX:+PrintTenuringDistribution
* -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
*/
public class Test4 {
public static void main(String[] args) throws InterruptedException {
byte[] b1 = new byte[512 * 1024];
byte[] b2 = new byte[512 * 1024];
myGC();
Thread.sleep(1000);
System.out.println("11111111");
System.out.println();
myGC();
Thread.sleep(1000);
System.out.println("2222222");
System.out.println();
myGC();
Thread.sleep(1000);
System.out.println("3333333");
System.out.println();
myGC();
Thread.sleep(1000);
System.out.println("4444444");
System.out.println();
byte[] b3 = new byte[1024 * 1024];
byte[] b4 = new byte[1024 * 1024];
byte[] b5 = new byte[1024 * 1024];
myGC();
Thread.sleep(1000);
System.out.println("555555");
System.out.println();
myGC();
Thread.sleep(1000);
System.out.println("666666");
}
public static void myGC() {
for (int i = 0; i < 40; i++) {
byte[] b = new byte[1024 * 1024];
}
}
}