目录
四、CMS垃圾回收器工作原理和Stop The World现象
一、分代收集理论
学习这个理念之前,我们要明白下面几点JVM常识:
1.绝大部分的对象都是朝生夕灭。---区域:新生代
2.对象熬过多次垃圾回收,越来越难回收。---区域:老年代
下面就从图中来介绍分代收集伦理,看图说话:
1.对象的的存活分代分为新生代和老年代,新生代又划分成三个区域(Eden、from、to区,至于为什么新生代为什么要这样分?别急会细细道来)
2.垃圾回收器的回收范围包括堆的新生代、老年代、别忘了 方法区 也是会回收的只是里面的内容很难回收。
3.不同的分代区采用不同算法的垃圾回收器,新生代(MinorGC/YoungGC)主要采用复制算法,老年代(MajorGc/OldGc)主要采用标记算法(标记清除算法或者标记整理算法)
4.垃圾回收器其实也是一个线程,负责的任务就是清楚垃圾对象。
5.任何区域满了都会发生GC的。
6.老年代发生GC的时候,一般会先发生一次FullGC,fullGC会回收新生代、老年代、方法区;
代码实操演示GC,打印GC日志:
public class StopWorld {
/*不停往list中填充数据*/
//就使用不断的填充 堆 -- 触发GC
public static class FillListThread extends Thread{
List<byte[]> list = new LinkedList<>();// 这里list就是一个GCRoots
@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();
}
}
在java运行的时候设置JVM打印日志的参数:
-XX:+PrintGCDetails
这是去查看控制台打印的日志信息:
二、垃圾回收器算法
1、复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉格式化。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
但是专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照图1中1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。(From和To统称survivor区)。
特点:1.实现简单,运行高效;2.内存复制,没有内存碎片;3.空间利用率只有一半(通过加入Eden区,解决利用率只有一半的问题,利用率可以达到90%,一般Eden:From:To=80:10:10)
注意:假如From区满了,发生一次GC,但是From区没对象需要清除,这是会将对象复制到To区,格式化From区,而这时Eden区也有对象要进入To区,这是是放不下的,就会使用上篇文章说到的空间分配担保策略,将年龄大点的对象直接进入老年代。
2、标记算法
标记清除算法:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
特点:1、执行不稳定(当90%要标记清除就很不好了;当10%要清除就还可以,所以适合老年代);2、产生大量内存碎片导致提前GC(因为碎片的内存,遇到放稍微大的对象的时候,找不到一段连续的空间存放这个对象就会发生GC),要知道GC很消耗性能的,系统也不是说动不动就发生GC。
标记整理算法:首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。
特点:1、对象移动 2、引用更新(移动了对象就要更新引用) 3、用户线程暂停 4、没有内存碎片
三、JVM中常见的垃圾回收器
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
从jvm的垃圾收集器历史发展以来,收集器的进步发展史:
1.单线程垃圾收集器
jvm刚诞生的时候是单线程的,不需要考虑多线程的协作,就是serial和serial old。
2.多线程并行垃圾收集器
随着jvm和手机的发展,内存越来越大,就诞生了多线程收集器,就诞生了Parallel Scavenge和Parallel Old。
3.多线程并发垃圾收集器
前面发展的单线程和多线程并行垃圾收集器在运行GC的时候,是要暂停所有用户线程的,也就是所谓的stop the world卡顿,因为不可能用户线程在边丢垃圾,回收垃圾线程边在回收,这样永远回收停不下来,所以就有了stop the world。解决这种影响用户体验的bug就诞生了多线程并发垃圾收集器,用户线程可以和回收线程并发工作,但也是部分时间并发的,CMS就是并发垃圾回收器。CMS对老年代,搭配ParNew回收新生代来使用的。
四、CMS垃圾回收器工作原理和Stop The World现象
CMS:Concurrent Mark Sweep 主要思路还是前面说的标记清除算法。只是把标记阶段分了好多小阶段,为的就是把stop the world的时间降到最小。
1.初始标记----暂停用户线程
标记的GCRoot的一些直接引用关联的对象(一级关联的对象),速度快,暂停用户工作线程的时间比较短;
2.并发标记----同时进行
标记的是一级关联对象后面的关联对象,可能有好多个,一级一级下去很深,这个过程就和用户线程同时进行,避免了stop the world;
3.重新标记----暂停用户线程
由于上面的用户线程和垃圾回收线程同时在跑了,会出现标记不干净的情况,所以就加了一个重新标记的过程(标记的都是要回收的对象),这个时候就要暂停用户线程,才能标记干净,是这么个道理吧?
上面三个标记过程类比生活中嗑瓜子的过程,你在嗑瓜子(用户线程),清洁人员在扫垃圾,第二个过程为了不让影响你嗑瓜子的体验,我可以边扫边嗑,但最后你总要暂停一下我才能扫干净吧?就是这么个过程。
4.并发清除----同时进行
清除也是比较耗时,所以也是用并发清除。
CMS特点:1、优点是减少了stop the world的时间,提升了用户体验;2、缺点:CPU敏感;浮动垃圾(在并发清除的时候用户线程会产生比较少的垃圾就成了浮动垃圾,假如浮动垃圾过多就会使用标记整理算法的serial Old代替CMS);内存碎片
五、总结
结合JMM(java momory model)和JVM系统的学习过程中,我们可以很好地了解整个底层知识。