GC与内存分配策略

GC需要完成三件事:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

对象已死吗

引用计数法

这个算法是这样判断对象是否存活的:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

它无法解决对象之间循环引用的问题,同时虚拟机也不是通过引用计数法来判断对象是否存活的

GC与内存分配策略

简单示例: 对象objA和objB都有属性instance,赋值令objA.instance = objB 及 objB.instance = objA, 除此之外,这两个对象再无其他引用,实际上着两个对象已经不可能再被访问,但是他们因为互相引用而导致对方的引用计数都不为0,于是引用技术算法无法通知GC收集器去回收它们。

可达性分析

GC与内存分配策略
GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(图论中即为从GC Roots到这个对象不可达)时,则证明对象是不可用的。

Java中,可作为GC Roots的对象包括以下:

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

引用的类型

无论是通过引用计数法去判断对象的引用数量,还是通过可达性分析算法去判断对象的引用链是否可达,这些都与“引用”有关。 Java中的引用有四种:

  • 强引用(StrongReference)

    在代码中普遍存在,类似 Object obj = new Object()这类引用; 只要强引用还存在,垃圾收集器永远不会回收被它引用的对象。

  • 软引用 (SoftReference)

    有用但非必要的对象,“食之无味,弃之可惜”。在内存空间还足够时,则能保存在内存之中,如果内存空间在垃圾收集后还是非常紧张,则可以抛弃这些对象。

  • 弱引用 (WeakReference)

    非必需的对象,被它引用的对象只能生存到下一次垃圾收集发生之前。

    当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。

  • 虚引用 (PhantomReference)

    最弱的一种引用关系,随时都可能被回收,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的时,在这个对象被收集器回收时收到一个系统通知。

    垃圾收集算法

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

GC与内存分配策略
该算法分为“标记”和“清除”两个阶段: 首先需要标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

问题: 会产生大量的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾收集动作。

复制算法

GC与内存分配策略
问了解决大量的内存碎片问题,复制算法就出现了。它将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

问题:

  • 直接将内存缩小了一半
  • 在对象存活率较高情况下,复制的代价比较大

现在很多虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间。而是将内存划分为一块大的Eden区和两块较小的Survivor区,比例为8:1:1。也就是说新生代中可用内存空间变成总容量的90%, 剩下的10%的空间用于放置每次存活的对象。但我们没有办法保证,每次存活的对象比例都小于10%,因此当Survivor空间不够用时,需要依赖其他内存进行分配担保。

标记-整理算法

GC与内存分配策略
先标记出需要回收的对象,之后将存活的对象都向一端移动,然后直接清理掉边界以外的内存。

分代收集算法

将Java堆分成新生代和老年代。

新生代: 大批对象死去,少量对象存活,可采用复制算法。(只需要付出少量存活对象的复制成本就可以完成收集)

老年代: 少量对象死去,大批对象存活,采用标记-清除或标记整理算法。(没有额外的空间对它进行担保)