深入理解java垃圾回收机制——

一、垃圾回收机制的意义

  Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
  ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。

垃圾回收机制中的发现算法(发现垃圾)

  Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。  

1.引用计数法(Reference Counting Collector)

1.1算法分析 
  引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.2优缺点
优点:
  引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
  无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
如下图:
image.png
A,B,C三个对象都不可能计数器为1,但是它们三个连在一块确实是垃圾,所以引用计数法不能将它们回收。

根搜索算法(可达性分析算法)

由于引用计数法存在缺陷,所有现在一般使用根搜索算法。
垃圾回收机制: - 图2
根搜索算法图解

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 如上图中的 ObjFObjDObjE通过 GC Root 是无法找到的,所以它们是无用节点。

Java 中可作为 GC Root 的对象:

  • 虚拟机栈中引用的对象(本地变量表)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象(Native对象)

    root可以简单的认为就是一个引用变量

    image.png

    垃圾回收算法

    在确定了哪些垃圾可以被回收后,垃圾收集器要做的就是进行垃圾的回收,有下面的几中算法:

    标记-清除(Mark-Sweep)算法

    标记-清除算法分为两个阶段:

  • 标记阶段:标记出需要被回收的对象。

  • 清除阶段:回收被标记的可回收对象的内部空间。

垃圾回收机制: - 图4
标记-清除算法图
标记-清除算法实现较容易,不需要移动对象,但是存在较严重的问题:

  • 算法过程需要暂停整个应用,效率不高。
  • 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

    复制(Copying)算法

    为了解决标志-清除算法的缺陷,由此有了复制算法。
    复制算法将可用内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。
    垃圾回收机制: - 图5
    复制算法图
    小结:

  • 优点:实现简单,不易产生内存碎片,每次只需要对半个区进行内存回收。

  • 缺点:内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。

    标记-整理(Mark-Compact)算法

    为了更充分利用内存空间,提出了标记-整理算法。
    此算法结合了“标记-清除”和“复制”两个算法的优点。
    该算法标记阶段和“标志-清除”算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
    垃圾回收机制: - 图6
    标志-整理算法图

    分代收集(Generational Collection)算法

    分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。
    核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
    垃圾回收机制: - 图7
    分代算法图
    区域划分:

    年轻代(Young Generation

    1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
    2. 新生代内存按照8:1:1的比例分为一个 eden 区和两个 survivor(survivor0,survivor1) 区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0 区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
    3. survivor1区不足以存放 edensurvivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC ,也就是新生代、老年代都进行回收。
      4.新生代发生的 GC 也叫做 Minor GCMinor GC 发生频率比较高(不一定等 Eden 区满了才触发)。

    年老代(Old Generation

    1. 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发 Major GCFull GCFull GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

    持久代(Permanent Generation 用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class ,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

GC 类型:

  1. Minor GC(新生代 GC):
    新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生熄灭的特点,所以 Minor GC 十分频繁,回收速度也较快。
  2. Major GC(老年代 GC):
    老年代 GC,指发生在老年代的垃圾收集动作,当出现 Major GC 时,一般也会伴有至少一次的 Minor GC(并非绝对,例如 Parallel Scavenge 收集器会单独直接触发 Major GC 的机制)。 Major GC 的速度一般会比 Minor GC 慢十倍以上。
  3. Full GC:
    清理整个堆空间—包括年轻代和老年代。Major GC == Full GC参考:聊聊JVM(四)深入理解Major GC, Full GC, CMS

产生 Full GC 可能的原因:

  1. 年老代被写满。
  2. 持久代被写满。
  3. System.gc() 被显示调用。
  4. 上一次 GC 之后 Heap 的各域分配策略动态变化。