3、垃圾回收算法与垃圾回收器
一、GC回收对象时,怎么判断对象的存活
1、引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。c++中的智能指针就是利用引用计数来实现的
。优点是快,方便,实现简单,但是存在一个循环引用的问题。缺陷:对象相互引用时(A.instance=B 同时 B.instance=A),很难判断对象是否该回收。
2、可达性分析(JDK中使用)
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
作为 GC Roots 的对象包括下面几种:
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 虚拟机栈(本地变量表)中引用的对象。
- 本地方法栈JNI(Native方法)中引用的对象。
2.1各种引用
- 强引用
一般的 Object obj = new Object() ,就属于强引用。(如果有 GCroots 的强引用)垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止 - 软引用
如果有GCroots的软引用,在内存不足GC时会被回收软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。 一些有用但是并非必需,用软引用关联的对象,系统将要发生 OOM 之前,这些对象就会被回收。
- 弱引用
如果有GCroots的弱引用,只要发生GC就会被回收 - 虚引用
幽灵引用,最弱,被垃圾回收的时候收到一个通知 如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动
二、GC(Garbage collection)
Minor GC
特点: 发生在新生代上,发生的较频繁,执行速度较快 触发条件: Eden 区空间不足\空间分配担保
Full GC
特点: 主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢 触发条件:
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC 处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发 Full GC
1、GC回收算法
1.1复制算法(Copying)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可, 实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
注意:内存移动是必须实打实的移动(复制),不能使用指针玩。为什么HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1
专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以一般来说回收占据 10%的空间够用了,所以并不需要按照 1:1 的比例来划分内存空间,而是 将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一 次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被 “浪费”。
优点
简单高效,不会出现内存碎片问题
缺点
内存利用率低,只有一半
存活对象较多时效率明显会降低
1.2标记-清除算法(Mark-Sweep)
过程:
- 首先标记所有需要回收的对象
- 统一回收被标记的对象
缺点:
1.效率问题,标记和清除效率都不高
2.标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不 提前触发另一次垃圾收集动作。
1.3标记-整理算法(Mark-Compact)
和清楚算法的区别是没有内存碎片,清理时会先将内存整理
三、JVM提供的垃圾回收器
分代收集:根据各个年代的特点选取不同的垃圾收集算法,新生代使用复制算法 老年代使用标记-整理或者标记-清除算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
1、各种垃圾回收器
1.1 Serial/Serial Old
最古老的,单线程,独占式,成熟,适合单 CPU 服务器
1.2 ParNew
和 Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少
1.3 Parallel Scavenge(ParallerGC)/Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
1.4 Concurrent Mark Sweep (CMS)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务 的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。
-XX:+UseConcMarkSweepGC ,一般新生代使用 ParNew,老年代的用 CMS
从名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些。
执行步骤主要分为以下四部
- 初始标记
仅仅是标记一下GCroots能直接关联到的对象,速度很快,会触发STW -Stop the world - 并发标记
从GCroots开始对堆中的对象进行可达性分析,找到存活的对象,耗时较长,和用户线程是并发运行的 - 重新标记
为了修正在并发标记期间用户线程继续运作而导致的标记变动的部分对象,在此时需要进行一次重新标记,需要STW -Stop the world - 并发清理
和用户线程并发处理,清理内存数据
优点:
- 整个过程中最耗时的两个操作:并发标记、并发清除都是和用户线程并发运行的。阻塞用户的时间大大减少
缺点:
- cpu资源敏感
因为并发阶段多线程占据 CPU 资源,如果 CPU 资源不足,效率会明显降低。 - 浮动垃圾
由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法 在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%)
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 会产生内存碎片
标记 - 清除算法会导致产生不连续的空间碎片
1.5 G1(G)
1.5.1 内存布局的改变
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
1.5.2 GC模式
Young GC
选定所有年轻代里的 Region。通过控制年轻代的 region 个数,即年轻代内存大小,来控制 young GC 的时间开销。(复制回收算法) 。
Mixed GC
选定所有年轻代里的 Region,外加根据 global concurrent marking 统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收 益高的老年代 Region。
Mixed GC 不是 full GC,它只能回收部分老年代的 Region。如果 mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就 会使用 serial old GC(full GC)来收集整个 GC heap。所以我们可以知道,G1 是不提供 full GC 的。
1.5.2 执行步骤:全局并发标记(Global Concurrent Marking)
- 初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的 Region 中创建对象,此阶段需要停顿线程(STW),但耗时很短。 - 并发标记
从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。 - 最终标记
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执 行。 - 筛选回收
首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起 并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
空间整合:不会产生内存碎片,算法:标记—整理 (humongous) 和复制回收算法(survivor)。
可停顿预测
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆 积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region (这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。