存活判定算法

引用计数算法(Reference counting)

每个对象都含有一个引用记数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。 垃圾回收器在含有全部对象的列表上遍历时,如果发现某个引用计数器的值为0时,就释放其占用的空间。 这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。
请看下面的代码:

  1. public class ReferenceCountingGC{
  2. public Object instance = null;
  3. public static void testGC() {
  4. ReferenceCountingGC objA = new ReferenceCountingGC();
  5. ReferenceCountingGC objB = new ReferenceCountingGC();
  6. objA.instance = objB;
  7. objB.instance = objA;
  8. objA = null;
  9. objB = null;
  10. //假设在这行发生GC, objA和objB是否会被回收?
  11. System.gc();
  12. }
  13. }

虽然objA和objB都已经为null,它们new出来的对象已经不可能再被访问,但是它们无法被回收,因为它们相互引用着对方,它们的引用计数器都不为0,于是引用计数器无法通知GC收集器回收它们。
因此Java虚拟机没有选用引用计数算法来管理内存。

可达性分析算法(Reachability Analysis)

通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,这些对象则被认为是可回收对象。
在Java语言中,可作为GC Roots的对象包括以几种:

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

    垃圾回收算法

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

    image.png
    首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
    存在的问题:
  1. 效率问题:标记和清除两个过程的效率都不高
  2. 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中无法找到足够的连续内存分配给较大对象,而不得不提前触发另一次垃圾收集动作。

    复制算法

    将内存划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活的对象复制到另一块内存上,然后把已使用过的内存空间一次清理掉
    优点:每次只对其中一块进行GC,不用考虑内存碎片的问题,并且实现简单,运行高效
    缺点:内存缩小了一半

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

    image.png
    标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有不可回收对象都向一端移动,然后直接清理掉边界以外的内存。

    内存泄漏

    虽然已经有gc了,但是由于代码本身的问题还是容易出现以下几种内存泄漏情形
  • 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露。 尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
  • 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
  • 清空堆栈中的某个元素,并不是彻底把它从数组中拿掉,而是把存储的总数减少,假如堆栈加了10个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。
  • 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。