垃圾回收

如何判断对象已经死亡

引用计数法

方式:计算每个对象被引用的次数,当次数不为0时,说明该对象被其它对象使用,则无法回收
弊端:若存在循环引用的情况,则内存无法被释放
旧的 python 解释器之前使用的就是这种方式

可达性分析法

方式:JVM中的垃圾回收器回扫描堆中所有的对象,并沿着GC root对象为起点的引用链进行寻找,若不在该链中的对象,则可以进行回收
可以作为 GC root 的对象

  • 虚拟机栈(栈帧中局部变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类的静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

    五种引用(强软弱虚 终结器)

    强引用

    若对象被强引用指向,垃圾回收时则不会回收该对象
    只有GC root都不引用的对象,才会被垃圾回收
    1. Student student = new Student();

    软引用

    垃圾回收时,若内存空间不足,则考虑回收软引用指向的对象
    若GC root只通过软引用指向对象,当内存空间不足时,该对象才会被垃圾回收
    注意:软引用指向的对象虽然会被回收,但软引用本身不会被回收
    想要清理软引用,则需要使用引用队列,当软引用对象被回收时,软引用本身则会加入到引用队列中,通过 queue.poll() 清空软引用占用的内存
    1. ReferenceQueue<Student> queue = new ReferenceQueue<>();
    2. //将软引用对象和队列进行关联
    3. SoftReference<Student> ref = new SoftReference<>(new Student(), queue);
    4. //...发生垃圾回收
    5. queue.poll();

    弱引用

    与软引用类似,只要发生垃圾回收时,弱引用指向的对象一定被回收,但弱引用本身不会被回收,也需要借助引用队列
    1. WeakReference<Student> ref = new WeakReference<>(new Student());

    虚引用

    通过虚引用并不能获取对象;为一个对象设置虚引用的目的只是为了能够在这个对象被垃圾回收时回去到系统通知虚引用必须和引用队列一起使用
    由于虚引用可以跟踪对象的回收时间,所以可以将一些资源释放操作放在虚引用中执行
    常用于配合消耗直接内存的对象使用
    从引用队列中直接调用释放内存的 clean 方法
    1. ReferenceQueue<Student> queue = new ReferenceQueue<>();
    2. WeakReference<Student> ref = new WeakReference<>(new Student, queue);

    终结器引用

    用于实现对象的 finalize() 方法,无需手动实现,内部配合引用队列使用
    再GC时,终结器引用入队,Finalizer 线程通过终结器引用找到相应的对象并调用 finalize() 方法,第二次 GC 时该对象才会被回收,效率较低

    垃圾回收算法

    标记-清除算法

    过程
  1. 将和 GC root 相关的对象标记出来
  2. 清除没有被标记的对象

优缺点:回收速度快;但会造成内存碎片,浪费空间

标记-整理算法

在标记清除的算法基础上新增了整理内存空间的功能,避免了垃圾回收造成的内存碎片
优缺点:没有了内存碎片;但回收速度变慢了

标记-复制算法

将内存区域划分成两部分:from 和 to,其中 to 区为空,from 区存有对象

  1. 将 GC root 引用的对象从 from 区移动到 to 区中,在 to 区中挨个存入,避免了内存碎片
  2. 清空 from 区
  3. 将 from 与 to 交换位置

优缺点:回收速度快、不会产生内存碎片;但是空间利用率低

分代回收算法

分代收集算法其实是以上三种垃圾回收算法的集合体,分代收集算法将整个堆内存划分成了不同的区域:新生代(Eden、Survivor From、Survivor To)、老生代
每个区域都会其功能采用不同的垃圾回收算法

新生代

Eden

用于存入新创建的对象
当 Eden 区中的内存不足时,会触发 Minor GC,Minor GC 会将 Eden 区和 S0 区中存活的对象存入 S1 区中,同时对象的年龄+1,随后清空 Eden 和 S0 区,再将S0与S1互换位置
Minor GC 会触发 stop the world,暂停其它用户线程,只运行垃圾回收线程

Survivor From(Surviror0)

用于存入 Eden 区中幸存的对象
如果发生GC时,Eden 和 Survivor From 中的空间都满了,Survivor 中存活的对象会被直接放入老年代中

Survivor To(Survivor1)

用于临时存入被整理好的对象,对象来源 Eden 和 Survivor From,存入以后与 Survivor From 交换位置,并清空重新成为空的 Survivor To 区

老年代

一般来说,大对象会直接存入老年代中
年龄超过15的对象会被存入老年代中
如果老年代中空间满了,先会触发Full GC,同时也会触发 Minor GC,扫描整个内存空间

垃圾收集器

垃圾收集器其实是垃圾回收算法的具体实现

Serial 收集器

image.png
单线程垃圾收集器,当 Serial 收集器被触发运行的时候,其它所有用户线程都需要停止工作(STW),直到垃圾收集器运行完毕
新生代采用的标记-复制算法,老年代采用的标记-整理算法
优缺点:简单高效;但是每次收集时对程序性能影响大

ParNew 收集器

image.png
ParNew 收集器是 Serial 收集器的多线程版本
新生代采用的标记-复制算法,老年代采用的标记-整理算法
优缺点:采用多线程,CPU利用率高;但是存在线程切换时的消耗

Parallel Scavenge 收集器

Parallel Scavenge 收集器与 ParNew 收集器比较类似,区别在于 Parallel Scavenge 收集器更注重吞吐量优先(用户线程占用CPU时间/CPU运行总时间)
与 ParNew 收集器一样,使用多线程新生代采用的标记-复制算法,老年代采用的标记-整理算法
相关参数:

  1. -XX:MaxGCPauseMillis,用于控制最大垃圾收集停顿时间,大于0的毫秒数;若设置过小,停顿时间会缩短,但可能会导致程序的吞吐量下降
  2. -XX:GCTimeRatio,用于设置垃圾收集时间占总时间的比率,取0-100间整数,计算公式为:垃圾收集时间占比=1/(1+n);默认为99,即1/(1+99)=0.01,即1%的时间用于垃圾收集
  3. -XX:+UseAdptiveSizePolicy,开启GC自适应策略,JVM会根据当前系统运行情况和性能监控信息,动态调整堆内存和晋升年龄等信息

    Serial Old 收集器

    Serial 收集器的老年代版本,使用单线程标记-整理算法
    在JDK1.5以前与 Parallel Scavenge 收集器搭配使用,或者作为 CMS 收集器的后备方案(JDK1.5以后被 Parallel Old 收集器替换)

    Parallel Old 收集器

    Parallel Scavenge 收集器的老年代版本,使用多线程标记-整理算法
    注重吞吐量优先的情况下,可以优先考虑使用 Parallel Scavenge 收集器和 Parallel Old 收集器(JDK1.5以后替换了单线程的 Serial Old 收集器)

    CMS 收集器

    image.png
    CMS(Concurrent Mark Sweep)收集器是一款注重响应时间优先的收集器,更加关注用户的使用体验(更少的停顿感受)
    CMS 收集器可能会发生 Concurrent Model Failure 错误,原因是CMS线程本身需要占用一部分堆空间,如果当用户线程运行的同时不断产生对象占满堆空间导致CMS无法工作,就会触发 Concurrent Model Failure 错误,此时会触发JVM后备预案,启用 Serial Old 收集器,进行一次 Full GC
    CMS 收集器真正实现了让垃圾收集线程与用户线程同时工作,收集步骤主要分为以下四步:

  4. 初始标记:暂停所有线程(STW),标记出与GC root相关的对象(存活对象),过程很快

  5. 并发标记:同时开启CMS标记线程和用户线程,在用户线程运行的同时去标记出GC root可达的对象;但是不能保存可达性分析的实时性,因为在用户线程会不断更新引用域;但是并发标记会把可能变动的对象标记成Dirty Card状态,减少了重新标记时的工作量;
  6. 重新标记:暂停所有线程(STW),修正并发标记期间以为用户线程运行造成的引用对象变动;花费时间比初始标记时间稍长,但比并发标记花费的时间短
  7. 并发清除:同时开启CMS清除线程和用户线程,采用标记-清除算法,对未标记的对象进行清理

优缺点:响应时间优先、并发收集、低停顿;但是对CPU资源敏感、无法处理浮动垃圾(在并发清除时,用户线程产生的垃圾对象称为浮动垃圾)、标记-清除算法会产生大量碎片空间

G1 收集器(Garbage First)

Garbage First 是一款多CPU和大容量内存的垃圾收集器,在吞吐量和响应时间上都有不错的表现,JDK9 上已经默认使用,替代了 CMS 收集器
G1 收集器将整个堆空间划分成了大小相等的独立区域(Region),每个 Region 都可以单独的作为Eden、Survivor 和 Old区
G1 收集器可以大致分为4个步骤:

  1. 初始标记:仅标记出GC root能够关联到的对象,需要 STW,但速度很快
  2. 并发标记:与用户线程同时运行进行 GC Root Tracing,标记出所有存活的对象(并不能准确标记出所有存活的对象),并将对象状态变化信息记录进Remembered Set Log
  3. 最终标记:根据Remembered Set Log中记录的对象变动记录,修正并发标记阶段没有被标记的存活对象,需要STW,采用多线程
  4. 筛选回收:G1收集器会在后台维护一个优先列表,会按照回收价值和成本排序待收集的 Region,根据用户期望的GC停顿时间来指定回收计划,最后按照计划回收价值较高的垃圾对象;回收时采用的是复制-清除算法,将存活的对象复制到空的 Region 中,并释放旧的 Region;这一步是和用户线程并发进行的

注意:当G1的老年代空间不足时,并且垃圾产生的速度大于垃圾回收的速度,则会触发 Full GC

ZGC 收集器

ZGC 收集器与 G1和ParNew类似,也采用了标记-复制算法,ZGC 出现 STW的情况会更少