GC回收机制

什么是垃圾

所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫做"**可达性分析”**的算法来决定对象是否能够被回收。算法

可达性分析

可达性分析算法是从离散数学中的图论引入的,JVM 把内存中全部的对象之间的引用关系看做一张图,经过一组名为”GC Root"的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后经过判断对象的引用链是否可达来决定对象是否能够被回收。以下图所示:数组

Cgq2xl58leGAIoKxAAEZWYE_v08477.png 好比上图中,对象A/B/C/D/E 与 GC Root 之间都存在一条直接或者间接的引用链,这也表明它们与 GC Root 之间是可达的,所以它们是不能被 GC 回收掉的。而对象M和K虽然被对J 引用到,可是并不存在一条引用链链接它们与 GC Root,因此当 GC 进行垃圾回收时,只要遍历到 J/K/M 这 3 个对象,就会将它们回收。markdown

注意:上图中圆形图标虽然标记的是对象,但实际上表明的是此对象在内存中的引用。包括GC Root也是一组引用而并不是对象。并发

GC Root对象

在Java中,有如下几种对象能够做为GC Root:
1.Java 虚拟机栈(局部变量表)中的引用的对象。
2.方法区中静态引用指向的对象。
3.仍处于存活状态中的线程对象。
4.Native 方法中 JNI 引用的对象。性能

何时回收

1.Allocation Failure:在堆内存中分配时,若是由于可用剩余空间不足致使对象内存分配失败,这时系统会触发一次 GC。
2.System.gc():在应用层,Java 开发工程师能够主动调用此 API 来请求一次 GC。spa

垃圾回收算法

标记清除算法

从”GC Roots”集合开始,将内存整个遍历一次,保留全部能够被 GC Roots 直接或间接引用到的对象,而剩下的对象都看成垃圾对待并回收,过程分两步。线程

  1. Mark 标记阶段:找到内存中的全部 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),不然标记为黑色(也就是垃圾对象)。
  2. Sweep 清除阶段:当遍历完全部的 GC Root 以后,则将标记为垃圾的对象直接清除。

标记清除.png

优势:实现简单,不须要将对象进行移动。
缺点:这个算法须要中断进程内其余组件的执行(stop the world),而且可能产生内存碎片,提升了垃圾回收的频率。3d

复制算法

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。以后,清除正在使用的内存块中的全部对象,交换两个内存的角色,完成垃圾回收。调试

  1. 复制算法以前,内存分为 A/B 两块,而且当前只使用内存 A,内存的情况以下图所示:

复制算法.png

  1. 标记完以后,全部可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存情况以下图所示:

复制算法2.png

  • 优势:按顺序分配内存便可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

标记-压缩算法

须要先从根节点开始对全部可达对象作一次标记,以后,它并不简单地清理未标记的对象,而是将全部的存活对象压缩到内存的一端。最后,清理边界外全部的空间。所以标记压缩也分两步完成:日志

  1. Mark 标记阶段:找到内存中的全部 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),不然标记为黑色(也就是垃圾对象)。
  2. Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。

标记压缩.png

  • 优势:这种方法既避免了碎片的产生,又不须要两块相同的内存空间,所以,其性价比比较高。
  • 缺点:所谓压缩操做,仍须要进行局部对象移动,因此必定程度上仍是下降了效率

JVM分代回收策略

Java 虚拟机根据对象存活的周期不一样,把堆内存划分为几块,通常分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代

分代回收的中心思想就是:对于新建立的对象会在新生代中分配内存,此区域的对象生命周期通常较短。若是通过屡次回收仍然存活下来,则将它们转移到老年代中。

年轻代

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集通常能够回收 70%~95% 的空间,回收效率很高。新生代中由于要进行一些复制操做,因此通常采用的 GC 回收算法是复制算法。

新生代又能够继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。这 3 块区域的内存分配过程以下:

绝大多数刚刚被建立的对象会存放在 Eden 区。如图所示:

新生代.png

当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。如图所示:

新生代-S0.png 下一次 Eden 区满时,再执行一次垃圾回收。这次会将 Eden和 S0区中全部垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。如图所示:

新生代s0-s1.png 如此反复在 S0 和 S1之间切换几回(默认 15 次)以后,若是还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。如图所示:

s0和s1来回切换15次.png

老年代

一个对象若是在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小通常比新生代大,能存放更多的对象。若是对象比较大(好比长字符串或者大数组),而且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

咱们可使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代由于对象的生命周期较长,不须要过多的复制操做,因此通常采用标记压缩的回收算法。

注意:对于老年代可能存在这么一种状况,老年代中的对象有时候会引用到新生代对象。这时若是要执行新生代 GC,则可能须要查询整个老年代上可能存在引用新生代的状况,这显然是低效的。因此,老年代中维护了一个 512 byte 的 card table,全部老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只须要检查这个 card table 便可,大大提升了性能。

GC Log 分析

为了让上层应用开发人员更加方便的调试 Java 程序,JVM 提供了相应的 GC 日志。在 GC 执行垃圾回收事件的过程当中,会有各类相应的 log 被打印出来。其中新生代和老年代所打印的日志是有区别的。

  • 新生代 GC:这一区域的 GC 叫做 Minor GC。由于 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 很是频繁,通常回收速度也比较快。
  • 老年代 GC:发生在这一区域的 GC 也叫做 Major GC 或者 Full GC。当出现了 Major GC,常常会伴随至少一次的 Minor GC。

注意:在有些虚拟机实现中,Major GC 和 Full GC 仍是有一些区别的。Major GC 只是表明回收老年代的内存,而 Full GC 则表明回收整个堆中的内存,也就是新生代 + 老年代。

总结:

虚拟机垃圾回收机制不少时候都是影响系统性能、并发能力的主要因素之一。尤为是对于从事 Android 开发的工程师来讲,有时候垃圾回收会很大程度上影响 UI 线程,并形成界面卡顿现象。所以理解垃圾回收机制并学会分析 GC Log 也是一项必不可少的技能。

相关文章
相关标签/搜索