第三章_1 垃圾收集器
一、概述
垃圾收集GC,这项技术当做java语言的伴生产物。
有三个问题需要回答:
1、哪些内存需要回收?
1)基于线程隔离的区域(操作数栈、本地变量表这些)不需要回收,因为它的生命周期是基于线程存在的,方法或线程结束,内存自然也跟着回收
2)堆空间需要进行回收。堆空间又分为堆区和非堆区,而堆区又分为新生代(朝生夕灭)和老年代。这些空间都需要进行回收。
2、什么时候回收?
1)当Eden区没有足够的空间进行分配时,触发新生代回收。
2)当Old区空间达到一定的界限时触发Full GC
3)方法区达到一定的界限时,触发GC
3、如何回收?
收回算法,分代回收原则。
二、如果判断对象是否已死
1、引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时就减1.任何时候计数器的值为0,就说明对象已死。
这个有个缺点,不能解决对象之间相互循环引用的问题。
objA.instance=objB
objB.instance=objA
2、枚举根节点,做可达性分析
凡事根节点不可达的对象,都可以进行收回。但不一定马上进行回收,具体得看不同GC的实现。
GC Roots包括哪些:
1)虚拟机栈(本地变量表)中引用的对象。
2)方法区静态属性引用的对象
3)方法区常量引用的对象
4)本地方法栈中 JNI(native方法)引用的对象
3、对象的死亡过程
两次标记,才能真正的宣告死亡
1)当进行完可达性分析之后,发现对象没有GC Roots的引用链,则进行第一次标记。
如果该对象对象覆盖了 finalize()方法,则把对象放到F-Queue队列中,并开启一个低优先级的线程去执行 F-Queue.在这个阶段如果发现有对象再次被引用,则成功拯救自己。
2)对F-Queue进行标记,没有成功拯救自己的对象,都会宣告死亡。
三、垃圾收集算法
- 新生代
1、复制算法
Survivor区,一块叫From,一块叫To,对象存在Eden和From块。当进行GC时,Eden存活的对象全移到To块,(也得看TO空间的大小是否够,不够用时进行分配担保,进入old区)
而From中,存活的对象按年龄值确定去向,当达到一定值(年龄阈值,通过-XX:MaxTenuringThreshold可设置)的对象会移到年老代中,没有达到值的复制到To区,经过GC后,Eden和From被清空。
之后,From和To交换角色,新的From即为原来的To块,新的To块即为原来的From块,且新的To块中对象年龄加1.
Eden和survivor内存大小比例为8:1
优点:
不用进行内存整理,直接进行复制,然后直接清除,实现简单高效。
缺点:
浪费点空间。因为S0或S1肯定有一个是空的。
对于朝生夕死的对象来说,效率还好。但对于存活率较高的情况下,如果频繁的来回复制效率也不会太高。所以复制算法比较适合新生代的垃圾收集。
- 老年代
2、标记-清除算法
标记和清除是两个步骤,当前是先标记后进行清除。
标记刚才讲过就是两次标记的过程,但这种算法有它的不足之处:
1、标记和清除的效率都不高
2、空间利用率问题,标记清除之后会产生大量的不连续内存碎片。可能导致以后分配大对象时发现没有连续的空间可用,不得不进行Full GC
3、标记-整理算法
标记:过程不用再说,和前面的一样都是两次标记的过程。
整理:让所有的存活的对象都向一端移动,然后清理掉端边界以外的内存。
优点:
标记-整理完之后,空间内不会有非连续的内存碎片。
- 分代收集法
4、分代收集法的核心思想
目前商业的GC收集器都采用的是分代收集的思想。
这个比较简单就是把堆划分为新生代和老年代,根据新生代和老年代的特点,使用不同的垃圾回收算法。
新生代:复制法
老年代:标记-清除法、标记-整理法
四、垃圾收集器
在介绍垃圾收集器之前先说一下怎么评价一款垃圾收集器的性能:
1)最小的停顿时间(stop the world)
在枚举根节点期间,要必免我在这分析,那边对象的引用又发生了变化,这种分析结果的准确性得不到保障。所以在进行可达性分析的时候,必须停顿所有的java执行线程。
2)最大的吞量
CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
根据这个公式可以看出,垃圾收集时间越小,吞吐量越高。性能越好。
1、Serial 收集器(现在很少见了)
单线程收集器、新生代、停顿时间长、复制算法
单线程收集:它进行垃圾收集的时,必须暂停其化所有的工作线程,直到它收集结束。
常见的组合:serial + serial old
2、ParNew 收集器
Serial收集器的多线程版本(并行收集)、新生代、复制算法
它时行收集的时候同样必须暂停所有的工作线程,直到它收集结束。不同的是Serial是只有一个线程进行收集,ParNew呢是多个线程进行收集。
常见的组合:ParNew + CMS + Serial Old(备用)
3、Parallel Scavenge 收集器
多线程并行收集、新生代、复制算法。这个同ParNew没啥大区别。它的特点在于:
1)它关注的是吞吐量,也称“吞吐量优先”收集器
2)自适应调节策略。只需要设置吞吐量和停顿时间参数,其它的交给收集器去做。
4、Serial Old 收集器(也很少用到了)
Serial 收集器的老年代版本。单线程、标记-整理算法
常见的搭配方案:
Serial + Serial Old
ParNew + CMS + Serial Old
5、Parallel Old 收集器(jdk1.6中才开始提供)
Parallel 收集器的老年代版本、多线程并行、标记-整理算法
搭配方案:
Parallel + parallel Old
6、CMS收集器
老年代、并发收集器(并发指用户线程与垃圾收集线程可同时执行。但并不表示可以不用stop the world 因为STW是在标记的阶段进行的。同样也是需要STW的。只是它追求的是最小的停顿时间)
标记-清除算法
特点:并发收集,低停顿
1)并发收集器
2)以获取最短回收停顿时间为目标的收集器
整个过程:
1)初始标记(需要stop the world)
标记GC Roots能关联到的对象
2)并发标记
GC Roots Tracing过程
3)重复标记(需要stop the world)
修正在并发标记阶段因用户线程继续运行导致的对象变化,进行重新标记
4)并发清除
缺点:
1)对cpu资源敏感
2)无法处理浮动垃圾
因是它是一边用户线程运行,一边垃圾收集,所以不能保障一次性收集干净,如果在运行期间预留的内存无法满足程序的需要,就会出现“Concurrent Mode Failure”失败,从而触发 Serial Old备用方案
3)基于标记-清除算法,会有大量的空间碎片。将会对大对象分配带来麻烦。不得不提交进行Full GC或者内存碎片合并整理过程
7、G1 收集器
新生代和老年代收集器、多线程并发收集、标记-整理算法
jdk1.7中开始提供,jdk1.8中比较成熟了,jdk1.9中是默认的收集器了。
处理大内存大于等于6G,还可以停顿时间非常的小。0.5秒以内
它把新生代和老年代当作是一个逻辑上的概念了,不再进行区分。
1)内存块(Region)
在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:
说一下Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
2)SATB(存活对象快照)
通过Roots Tracing得到的,GC开始时候存活对象的快照。这个干什么用呢,就是帮助对象回收。
3)Rset 记录Region之间的对象相互引用关系
记录谁引用了我的对象,避免全堆扫描。在GC根节点枚举范围中加入Rset即可保证不对全堆扫描也不会有遗漏。
4)YoungGC这一块没有变化:
5)MixedGC
global concurrent marking 全局并发标记,这个跟 CMS的标记阶段很像
当堆的占有率达到45%时,并发Mixed GC 并发标记,不代表会进行MixedGC,
还有一个参数控制着是进行YGC还是进行MixedGC