垃圾收集器与内存分配策略

2024-01-19 10:10:50 浏览数 (2)

概述

垃圾收集需要完成的三件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?
判断对象是都存活的算法:
  1. 引用计数法:在对象中添加一个引用计数器,每当有一个地方引用时,计数器值就加1;引用失效时,计数器值就减1。任何计数器为0的对象就是不可能再被使用的。但该算法无法解决循环引用问题。
  2. 可达性分析算法:通过一系列可称为“GC Roots”的根对象作为起点集,从这些节点开始,根据引用关系向下搜索,搜索过程中所走过的路径叫做“引用链”。
再谈引用:
  1. 强引用:类似于“new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
  2. 软引用:描述一些有用,但非必须的对象。软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。使用SoftRefrence类来实现软引用
  3. 弱引用:也是描述非必须对象,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。使用WeakRefrence类来实现弱引用。
  4. 虚引用:最弱的一种引用关系。无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的 唯一目的是为了能在这个对象被收集器回收时收到一个系统通知。使用PhantomRefrence类来实现虚引用。

一次对象自我拯救的演示:

代码语言:javascript复制
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        // 对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize()优先级很低,暂停0.5秒,以等待它执行
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead :(");
        }

        // 下面这段代码与上面完全相同,但是这次却自救失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize()优先级很低,暂停0.5秒,以等待它执行
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead :(");
        }
    }
}

---- 运行结果:

代码语言:javascript复制
finalize method executed!
yes, I am still alive :)
no, I am dead :(

上面示例代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败了,这里任何一个finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法就不会再次执行。

分代收集理论
  1. 弱分代假说:绝大部分对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集过程的都想越难以消亡

不同分代的垃圾收集名词:

  1. 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
    1. 新生代收集(Minor GC/Yong GC):指目标只是新生代的收集
    2. 老年代收集(Major GC/Old Gc):指目标只是老年代的收集。目前只有CMS收集器会有单独收集老年代的行为。
    3. 混合收集(Mixed GC):指目标时收集整个新生代以及部分老年代的垃圾收集器。目前只有G1收集器会有这种行为。
  2. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
垃圾收集算法
标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。也可以反过来,标记存活的对象,统一回收未被标记的对象。主要有如下两个缺点:

    1. 执行效率不稳定,标记和清除的执行效率会随对象数量的增长而降低
    2. 内存空间的碎片化问题,若需要分配较大对象时,由于无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法

新生代采用该算法。半区复制,可用内存缩小为了原来的一半,会造成较大的空间浪费。

HotSpot虚拟机的Serial、ParNew等新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和用过的Survivor空间,HotSpot虚拟机默认的Eden和Survivor的大小比例为8 : 1,也就是只有10%的新生代是被“浪费”的。若Survivor空间不足以容纳一次Minor GC之后存活的对象,则需要依赖其它内存区域(实际上大多是老年代)进行分配担保。

标记-整理算法

与标记-清除算法的本质差异在于标记-整理算法是一种移动式的回收算法。是否移动存活对象是一项优缺点并存的风险决策。

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新引用这些对象的地方,将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。也被形象称为“Stop The World”。若不移动存活的对象,则空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决,由于内存访问使用户程序最为频繁的操作,若在这个环节增加了额外的负担,则会大大影响应用程序的吞吐量。

HotSpt虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的。而关注延迟的CMS收集器则是基于标记-清除算法的。另外还有一种”和稀泥式“的解决方案,可以让虚拟机平时大多数时间都是采用标记-清除算法,暂时容忍内存碎片的存在,知道内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。CMS收集器就是采用的这种方式。

经典的垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的实践者。

上图展示了七种作用于不同分代的收集器。如果两个收集器之间存在连线,就说明他们可以搭配使用。这个关系不是一成不变的,JDK8中已将Serial CMS、ParNew Serial Old这两个组合申明为废弃。

0 人点赞