垃圾回收之垃圾标记算法

对象被判定为垃圾的标准:

  • 该对象没有被其他对象引用

判定对象是否为垃圾的算法:

  • 引用计数算法,优点:执行效率高,程序执行受影响较小;缺点:无法检测出循环引用的情况,容易导致内存泄露
    • 通过判断对象的引用数量来决定对象是否可以被回收
    • 每个对象实例都有一个引用计数器,被引用则+1,完成引用即引用结束则-1
    • 综上,任何引用计数为0的对象实例就可以被当作垃圾收集
  • 可达性分析算法(Java使用的算法)
    • 通过判断对象的引用链是否可达来决定对象是否可以被回收

可达性分析算法遍历引用链如图:

可以作为GC Root的对象:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性所引用的对象
  • 本地方法栈中JNI(Native)的引用对象
  • 活跃线程的引用对象,即线程对象

Java垃圾回收之回收算法

光有垃圾标记算法还不行,JVM还需要有垃圾回收算法来将这些标记为垃圾的对象给释放回收掉。主要的回收算法有以下几种:

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

  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存

缺点:由于标记 - 清除不需要进行对象的移动,并且仅对不可达的对象进行处理,因此使用该回收算法之后会产生大量不连续的内存碎片。而内存碎片过多可能会导致以后在程序运行过程中,需要分配内存给较大的对象时,无法找到足够大的连续内存空间,从而不得不再次触发垃圾回收工作,若依旧无法分配内存的话就会触发内存溢出异常。

1.复制算法(Copying):

  • 将可用的内存容量按一定比例划分为两块或多块,并将其中一块或两块作为对象面,剩余的则作为空闲面
  • 对象在对象面上创建,当对象面的内存空间用完时,会将存活的对象从对象面复制到空闲面中,接着清除该对象面中所有的对象

优点:解决内存碎片化问题,顺序分配内存,简单高效。该算法适用于对象存活率低的场景,所以普遍应用在新生代中,因为新生代里的对象存活率通常情况下只有10%左右

3.标记 - 整理算法(Compacting):

  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

优点:避免了标记 - 清除算法所带来的内存不连续性问题,以及不需要像复制算法那样需要设置两块内存互换。该算法适用于对象存活率较高的场景,所以普遍应用在老年代中,因为老年代里对象存活率较高

4.分代收集算法(Generational Collector):

  • 实际是多种垃圾回收算法的组合拳,该算法对堆内存进行进一步的划分,按照对象生命周期的不同划分区域以采用不同的垃圾回收算法。目的是提高JVM的回收效率,也是目前JVM使用的回收算法

在JDK7及之前的JVM版本共有三个分代,即新生代、老年代和永久代(注意,永久代不存在于堆中,而是存在于方法区):

而JDK8及以后的版本只有新生代和老年代:

分代收集算法的GC分为两种:

  • Minor GC,发生在新生代中的垃圾收集工作,采用的是复制算法
  • Full GC,当触发老年代的垃圾回收的时候通常也会伴随着对新生代堆内存的回收,即对整个堆进行垃圾回收,这便是所谓的FullGC

新生代用于尽可能快速地收集掉那些生命周期短的对象,新生代分为两个区:

  • Eden区:新创建的对象通常存放在这个区域,如果新创建的对象太大放不进该区域则会放进Survivor区或老年代中
  • 两个Survivor区,该区域又分为to和from区,至于谁是to区谁是from区则不是固定的,而是会随着GC互相转换

对象如何晋升到老年代:

  • 新生代里的对象每经历一次Minor GC并且存活,其年龄就会+1,当经历一定Minor GC次数依然存活的对象就会晋升到老年代,默认是15岁,即默认经历了15次Minor GC依旧存活的对象会被放到老年代
  • Survivor区中存放不下的对象会被直接放到老年代
  • 新生成的大对象会被直接放到老年代(通过参数:-XX:+PreetenuerSizeThreshold 设置)

常用的调优参数:

  • -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
  • -XX:NewRatio:老年代和新生代内存大小的比例
  • -XX:MaxTenuringThreshold:对象从新生代晋升到老年代经过GC次数的最大阈值

综上,老年代用于存放生命周期较长的对象,老年代采用的是标记 - 整理算法。

Full GC和Major GC:

  • Major GC通常是跟full GC是等价的,即收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是仅仅针对老年代的 GC
  • Full GC比Minor GC慢,但执行频率低

触发Full GC的条件:

  • 老年代空间不足
  • 永久代空间不足(JDK7及之前的版本)
  • 使用CMS GC时出现promotion failed,concurrent mode failure
  • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
  • 在代码里调用System.gc(),该方法只能作为提醒,具体是否触发Full GC还得看JVM
  • 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC

注:promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。


Java垃圾回收之新生代垃圾收集器

在了解垃圾收集器之前,我们需要知道一个概念“Stop-the-World”:

  • 该单词的含义即:JVM由于要执行GC而停止了应用程序的执行
  • 并且任何一个GC算法中都会发生
  • 实际上多数GC优化就是通过减少Stop-the-World发生的时间来提高程序性能

除此之外,我们需要知道什么是Safepoint:

  • 分析过程中对象引用关系不会发生变化的点,即安全点(Safepoint)。因为JVM不能随随便便就发生Stop-the-World,而是到一个相对安全的点才会发生
  • 产生Safepoint的地方:方法调用、循环跳转、异常跳转等等
  • 安全点数量得适中

JVM的运行模式:

  • Server:启动速度较慢,但是启动完成进入稳定期之后的运行速度比Client快,因为Server模式采用的是重量级的JVM,有比Client模式更多的优化
  • Client:启动速度较快,采用的是轻量级的JVM

各垃圾收集器之间的联系,即可以搭配使用关系:

Serial收集器(启动参数:-XX:+UseSerialGC,采用复制算法):

  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
  • 简单高效,是Client模式下默认的新生代收集器

ParNew收集器(启动参数:-XX:+UseParNewGC,采用复制算法):

  • 除了是采用多线程进行垃圾回收外,其余行为、特点与Serial收集器一样;是Server模式下首选的年轻代垃圾收集器
  • 单核执行效率不如Serial,需要在多核环境下执行才有优势
  • 该收集器默认开启的垃圾收集线程数与CPU核心数量相同

Parallel Scavenge收集器(启动参数:-XX:+UseParallelGC,采用复制算法):

  • 该收集器也是多线程的,只不过比起之前所介绍的收集器关注用户线程停顿时间,该收集器更关注系统的吞吐量
    • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
  • 同样在多核环境下执行才有优势,Server模式下默认的新生代收集器

Java垃圾回收之老年代垃圾收集器

Serial Old收集器(启动参数:-XX:+UseSerialOldGC,采用标记 - 整理算法):

  • 该收集器是Serial收集器的老年代版本,也是单线程收集,进行垃圾收集时,必须暂停所有工作线程
  • 简单高效,Client模式下默认的老年代收集器

Parallel Old收集器(启动参数:-XX:+UseParallelOldGC,采用标记 - 整理算法):

  • 该收集器是Parallel Scavenge收集器的老年代版本,也是多线程收集,同样是吞吐量优先
  • 通常配合新生代的Parallel Scavenge收集器使用

CMS收集器(启动参数:-XX:+UseConcMarkSweepGC,采用标记 - 清除算法):

  • 多线程收集,停顿时间优先,通常配合新生代的ParNew收集器使用

CMS收集器收集流程:

  1. 初始标记:会触发stop-the-world,但是停顿时间很短
  2. 并发标记:并发追溯标记,程序不会停顿
  3. 并发预清理:查找执行并发标记阶段从新生代晋升到老年代的对象
  4. 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
  5. 并发清理:清理垃圾对象,程序不会停顿
  6. 并发重置:重置CMS收集器的数据结构

CMS收集器图示:

G1收集器(启动参数:-XX:+UseG1GC,采用复制 + 标记 - 整理算法):

  • 该收集同时用于新生代和老年代,该收集器的目标在于替换掉CMS,并且采取了某些不同的方式跨越了新生代和老年代的边界
  • 将整个Java堆内存划分成多个大小相等的Region,即新生代和老年代不再是物理隔离的了
  • G1收集器全称为Garbage First,该收集器的特点如下:
    • 并行和并发
    • 分代收集
    • 空间整合
    • 可预测的停顿


Java垃圾回收之常见面试题

1.Object的finalize()方法的作用是否与C++的析构函数作用相同:

  • 与C++的析构函数不同,析构函数调用确定,而finalize()方法是不确定的,因为finalize()方法在对象被GC回收时调用
  • 将未被引用的对象放置于F-Queue队列
  • 该方法执行随时可能会被终止
  • 它的设计目的是保证对象在被垃圾收集前完成特定资源的回收或给予对象最后一次重生机会等

示例代码:

package com.example.demo.gc;

/**
 * @author 01
 * @date 2019-07-18
 **/
public class Finalization {

    public static Finalization finalization;

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
        finalization = this;
    }

    public static void main(String[] args) {
        Finalization f = new Finalization();
        System.out.println("First print: " + f);
        f = null;
        System.gc();
        System.out.println("Second print: " + f);
        System.out.println(f.finalization);
    }
}

执行结果:

从执行结果可以看到,Finalization对象被GC回收时finalize()方法会被调用,finalize()方法里将当前对象this赋值给了静态属性finalization实现了对象的“重生”,所以在GC之后依旧能打印到该对象的地址信息

注:finalize是个不太可控的方法因此并不常用,并且在JDK9+版本被标注为过时方法

2.Java中的强引用,软引用,弱引用及虚引用有什么用:

  • 强引用(Strong reference):

    所谓强引用,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。总结:

    • 最普遍的引用,例:Object obj = new Object();
    • JVM宁可抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
    • 通过将对象设置为null来弱化引用,使其被回收
  • 软引用(Soft reference):

    是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。总结:

    • 对象处在有用但非必须的状态
    • 只有当内存空间不足时,GC会回收该引用的对象内存
    • 软引用通常用来实现内存敏感的高速缓存
    • 可以配合引用对象使用(ReferenceQueue)
  • 弱引用(Weak reference):

    弱引用并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。总结:

    • 用于描述非必须的对象,比软引用更弱一些
    • 发生GC时就会被回收掉,不过被回收的概率也不大,因为GC线程优先级比较低
    • 适用于引用偶尔被使用且不影响垃圾收集的对象
    • 可以配合引用对象使用(ReferenceQueue)
  • 虚引用(Phantom reference),也被称为幻象引用:

    对于虚引用,你不能通过它访问对象。虚引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,例如 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的创建和销毁。总结:

    • 不会决定对象的生命周期
    • 虚引用的对象任何时候都可能被垃圾收集器回收,就像是没有引用的对象一样
    • 虚引用通常用来跟踪对象被垃圾收集器回收的活动,起哨兵作用
    • 与软引用和弱引用不同的是,该引用必须与引用对列(ReferenceQueue)联合使用

软引用代码示例:

// 强引用
String str = new String("abc");
// 转换为软引用
SoftReference<String> softReference = new SoftReference<>(str);

弱引用代码示例:

String str = new String("abc");
// 弱引用
WeakReference<String> weakReference = new WeakReference<>(str);

虚引用代码示例:

String str = new String("abc");
// 引用队列
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 转换为虚引用
PhantomReference<String> phantomReference = new PhantomReference<>(str, queue);
// GC在回收一个对象时,如果发现该对象存在虚引用,那么在回收之前会先将该对象的虚引用添加到与该对象关联的引用队列中;程序代码可以通过判断引用队列是否已加入虚引用来得知被引用的对象是否已经被回收

引用队列(ReferenceQueue):

  • ReferenceQueue无实际的存储结构,其存储逻辑依赖于内部节点之间的关系来表达
  • 存储关联的且被GC后的软引用,弱引用以及虚引用

引用强度关系:

  • 强引用 > 软引用 > 弱引用 > 虚引用

下面流程图简单总结了对象生命周期和不同可达性状态,以及不同状态可能的改变关系:

上图的具体状态,实际是 Java 定义的不同可达性级别(reachability level),在之前也说过判断对象可达性,是 JVM 垃圾收集器决定如何处理对象的一部分考虑。可达性具体含义如下:

  • 强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。
  • 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
  • 弱可达(Weakly Reachable),类似前面提到的,就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。这是十分临近 finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。
  • 幻象可达(Phantom Reachable),上面流程图已经很直观了,就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
  • 当然,还有一个最后的状态,就是不可达(unreachable),意味着对象可以被清除了。

各引用包装类的继承关系图:


下面我们来用一个例子演示引用包装对象及引用队列的使用,首先定义一个普通的类,并且实现finalize方法以便我们在测试时可以看到该对象是否被GC回收了:

package com.example.demo.gc;

/**
 * @author 01
 * @date 2019-07-18
 **/
public class NormalObject {

    public String name;

    public NormalObject(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalizing obj: " + name);
    }
}

然后定义一个WeakReference的子类,目的是扩展name属性,以便我们在测试时能够得知是哪个对象的引用对象:

package com.example.demo.gc;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

/**
 * @author 01
 * @date 2019-07-18
 **/
public class NormalObjectWeakReference extends WeakReference<NormalObject> {

    public String name;

    public NormalObjectWeakReference(NormalObject referent, ReferenceQueue<NormalObject> queue) {
        super(referent, queue);
        this.name = referent.name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalizing NormalObjectWeakReference: " + name);
    }
}

最后编写一个测试类:

package com.example.demo.gc;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 01
 * @date 2019-07-18
 **/
public class ReferenceQueueTests {
    // 引用队列
    private static ReferenceQueue<NormalObject> queue = new ReferenceQueue<>();

    /**
     * 检查引用队列里有没有引用对象,有的话则打印相关信息
     */
    private static void checkQueue() {
        Reference<? extends NormalObject> reference;
        while ((reference = queue.poll()) != null) {
            // 存在于引用队列中的引用对象
            System.out.println("In Queue: " + ((NormalObjectWeakReference) (reference)).name);
            // 获取引用的对象实例
            System.out.println("reference object: " + reference.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        List<WeakReference<NormalObject>> weakReferenceList = new ArrayList<>();
        // 创建引用对象
        for (int i = 0; i < 3; i++) {
            NormalObject normalObject = new NormalObject("Weak-" + i);
            NormalObjectWeakReference reference = new NormalObjectWeakReference(normalObject, queue);
            weakReferenceList.add(reference);
            System.out.println("Create weak: " + reference);
        }

        System.out.println("\nbefore gc ------");
        checkQueue();

        System.out.println("\ngc ing... ------");
        System.gc();
        // 让线程休眠一会,以保gc能够正常执行完毕
        Thread.sleep(1000);

        System.out.println("\nafter gc ------");
        checkQueue();
    }
}

运行结果:

可以看到在GC执行之前调用checkQueue方法没有打印任何信息,因为此时引用队列中没有任何引用对象。而当GC执行之后,引用队列中就被添加了与之相关联的引用对象,所以就能够打印出引用对象的相关信息


GC相关参考文章: