垃圾回收

一个跟踪过程,它传递性地跟踪指向当前使用的对象的所有指针,以便找到可以引用的所有对象,然后重新使用在此跟踪过程中未找到的任何堆内存。公共语言运行库垃圾回收器还压缩使用中的内存,以缩小堆所需要的工作空间

JAVA对象生命周期

内存回收API

内存分配与回收

Minor GC 和 Full GC

内存分配策略

graph LR
  start -- new --> A{栈?}
  A --> |Y| 栈
  A --> |N| B{大?}
  B --> |Y| O
  B --> |N| C{TLAB?}
  C --> |Y| E
  C --> |N| E
  E --> D{GC清除?}
  D --> S1
  S1 --> D
  S1 --> F{Age?}
  F --> |Y| O
  F --> |N| S2
  S2 --> D
  D --> |Y| 结束
  O -- fgc --> 结束
  栈 -- pop --> 结束
// -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class AllocationWithEden {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
    }
}
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 */
public class LargeObjectWithOld {
    public static void main(String[] args) {
        byte[] allocation;
        allocation = new byte[8 * 1024*1024]; // //直接分配在老年代中
    }
}
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
 */
public class OldObjectWithOld {
    private static int _1MB = 1024*1024;
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuring-Threshold设置
                allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
}
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
 */
public class OldObjectWithHalfSpace {
    private static int _1MB = 1024*1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
}

对象已死

引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收

虽然占用了一些额外的内存空间来进行计数,它的原理简单,判定效率也很高。但这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作

可达性分析算法

基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,对象之间的联系称为引用链,如果某个对象无法从 GC Root到达,则证明此对象是不可能再被使用的

stateDiagram-v2
  state GCRoots {
    线程栈变量
    静态变量
    常量池
    JNI指针
  }
  常量池 --> H
  JNI指针 --> H
  I
  E --> F
  F --> E
  静态变量 --> D
  线程栈变量 --> A
  A --> B
  B --> C

可以作为GCRoots的对象包括下面几种:

除了这些固定的GC Roots集合以外,根据垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合

引用

stateDiagram-v2
  对象创建 --> 对象初始化
  对象初始化 --> 强引用
  强引用 --> 弱引用
  强引用 --> 软引用
  弱引用 --> 强引用
  软引用 --> 强引用

  软引用 --> finalize
  强引用 --> finalize
  弱引用 --> finalize
  
  finalize --> 虚引用
  虚引用 --> 不可达

利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态

强引用

Object obj = new Object(); Object obj2 = obj; 

软引用

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

应用场景:缓存

弱引用

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

作用在于当强引用丢失之后,这个对象就会被回收

Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

使用弱引用而非强引用可以避免当 ThreadLocal 变量置为 null时,ThreadLocalMap 中的 key 不会还指着 ThreadLocal 变量

内存泄漏的情况在于,ThreadLocal 变量被回收,即 key 的值变成null, 但是 ThreadLocalMap 仍持有着对 Entry 的引用,此时发生内存泄露

除了手动调用 remove 方法之外,只要保证有着对 ThreadLocal 变量的强引用,也能避免内存泄露

虚引用

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

这里虚引用get永远会返回null

应用场景:管理堆外内存

引用队列

创建各种引用并关联到相应对象时,可以选择是否需要关联引用队列,JVM 会在特定时机将引用 enqueue 到队列里,我们可以从队列里获取引用进行相关后续逻辑

var que = new ReferenceQueue<>();
var a = new Object();
WeakReference<Object> ref = newWeakReference<>(a, que);
a = null;
System.gc();
Reference<?> ref1 = que.remove();
if (ref1 != null) {
    System.out.println(ref1.get());
}

总结

引用类型 强引用 软引用 弱引用 虚引用
类型 正常赋值 SoftReference WeakReference PhantomReference
回收时间 不回收 内存紧张时回收 GC就回收 随时可能被回收

方法区的回收

方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。 为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

常量回收:如果一个常量没有再被使用,那么就可以被回收

类的卸载,需要满足三个条件才有可能被回收:

可以控制Xnoclassgc参数让HotSpot进行回收类

垃圾回收算法

垃圾回收类型:

分代收集理论

前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论

建立在以下两个分代假说上:

标记-清除算法

GC 标记-清除算法由标记阶段和清除阶段构成。在标记阶段会把所有的活动对象都做上标记,然后在清除阶段会把没有标记的对象,也就是非活动对象回收,该算法一般应用于老年代,因为老年代的对象生命周期比较长

后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的

202031685456

标记-复制算法

把内存分为两个空间一个是From空间,一个是To空间,对象一开始只在From空间分配,To空间是空闲的。GC时把存活的对象从From空间复制粘贴到To空间,之后把To空间变成新的From空间,原来的From空间变成To空间,这也是JVM年轻代所使用的的回收算法

20203169025

商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。 当进行GC时,将Eden与使用过的Survivor中存活的对象移动到另外一个Survivor中,然后清除Eden与使用过的Survivor

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%

标记-整理算法

其中标记阶段跟标记-复制算法中的标记阶段是一样的,而对于整理阶段,它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的

但这种方式需要移动大量对象,处理效率比较低,同时也会STW

是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算

20203168564

分代收集

除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型 G1是逻辑分代,物理不分代 除此之外不仅逻辑分代,而且物理分代

批注 2020-05-08 164208

不同对象使用不同的回收算法

HotSport算法细节实现

根节点枚举

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行

在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来保存着对象的引用

安全点

导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,HotSpot通过只记录位于安全点的指令的方式来让其他线程在这个点开始进行垃圾回收

线程会通过主动式中断,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起,这个时候线程就是暂停起来的

HotSpot将这个轮询操作精简到了一条汇编指令

安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,这个区域可以安全地进行垃圾回收

如果线程要离开安全区域,要检查虚拟机是否已经完成了根节点枚举,如果完成了枚举,就可以离开,否则就要一直等待 直到收到了可以离开的信号

记忆集与卡表

垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围

记忆集的实现精度有:

屏幕截图 2020-10-19 140228

通过卡表就可以使用比较少的内存来记录,一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,脏卡页在垃圾回收时也会一起被扫描

写屏障

HotSpot通过写屏障(和并发操作的内存读写屏障非同一概念),这里的写屏障类似于虚拟机在解释字节码时的AOP环绕通知,即插入一条修改指令,修改内存的同时更新卡表

三色标记

把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

屏幕截图 2020-10-19 142719

当以下两个条件同时满足,则会出现将原本应该是黑色的对象被误标为白色:

增量更新破坏第一个条件:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了

原始快照破坏第二个条件:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现

内存分配

内存配置常见参数

-XX:+PrintGC      每次触发GC的时候打印相关日志
-XX:+UseSerialGC      串行回收
-XX:+PrintGCDetails  更详细的GC日志
-Xms               堆初始值
-Xmx               堆最大可用值
-Xmn               新生代堆最大可用值
-XX:SurvivorRatio  用来设置新生代中eden空间和from/to空间的比例.
-XX:NewRatio       配置新生代与老年代占比 1:2
含以-XX:SurvivorRatio=eden/from=den/to
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,
这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
-XX:SurvivorRatio     用来设置新生代中eden空间和from/to空间的比例.

OutOfMemoryError异常

java.lang.OutOfMemoryError: Java heap space 堆内存溢出

因为堆内存无法满足内存申请需要

设置堆内存大小解决-Xmx

java.lang.StackOverflowError  栈内存溢出

由于方法调用栈过深

设置线程最大调用深度-Xss

内存溢出与内存泄漏

内存溢出就是申请的内存大小超出了系统所能提供的,系统不能满足需求,于是产生溢出

内存泄漏是使用过的内存空间没有被及时释放,长时间占用内存,最终导致内存空间不足,而出现内存溢出

垃圾收集器

衡量垃圾收集器的三项最重要的指标是:

截止到JDK14,当前JAVA已有的垃圾回收器

批注 2020-05-08 163037

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 20G
  4. G1 - 上百G
  5. ZGC - 4T - 16T(JDK13)

连线表示垃圾收集器可以配合使用

垃圾回收器的发展路线是随着内存越来越大演进以及从分代算法演化到不分代算法

除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行

串行垃圾收集器Serial

2020316989

[120.792s][info   ][gc           ] GC(25) Pause Young (Allocation Failure) 10M->6M(15M) 0.936ms
[120.792s][info   ][gc,cpu       ] GC(25) User=0.00s Sys=0.00s Real=**0.00s**

通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器

ParNew

20203169926

它是 Serial 收集器的多线程版本

[GC (Allocation Failure) [ParNew: 4928K->512K(4928K), 0.0024282 secs] 7129K->3525K(15872K), 0.0024673 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

参数控制:-XX:+UseParNewGC 指定使用ParNew收集器

-XX:ParallelGCThreads 限制线程数量

Parallel Scavenge

其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器 吞吐量指的是CPU用于运行用户程序的时间占总时间的比值

停顿时间短,回收效率高,对吞吐量要求高

-XX:-UseParallelGC(年轻代) 和 -XX:+UseParallelOldGC(老年代)

Serial Old

Serial 收集器的老年代版本,给 Client 场景下的虚拟机使用 屏幕截图 2020-10-19 145451

如果用在Server端:是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

Parallel Old

202031692117

Parallel Scavenge 收集器的老年代版本

CMS垃圾收集器

202031692235

(Concurrent Mark Sweep)

CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上

stateDiagram-v2
  闲置等待 --> 初始化标记
  初始化标记 --> 并发标记
  并发标记 --> 预处理
  预处理 --> 重新标记
  重新标记 --> 并发清理
  并发清理 --> 调整堆大小
  调整堆大小 --> 重置
  重置 --> 闲置等待

通过JVM参数 -XX:+UseConcMarkSweepGC设置

G1垃圾收集器

一款面向服务端应用的垃圾收集器 目标是替代CMS

屏幕截图 2020-09-19 104157

G1将堆空间划分成若干个相同大小的区域 不同的区域存放不同的对象

几个问题:

过程:

  1. 初始标记 STW 标记GC根可达对象
  2. 根区域扫描 从上一阶段标记的存活区域扫描老年代对象
  3. 并发标记 对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象
  4. 最终标记 STW 完成最终的标记处理 用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
  5. 清理 STW 统计所有存活对象 并将回收区域排序 优先回收垃圾最多的区域

屏幕截图 2020-10-20 142254

G1使用建议:

通过JVM参数 -XX:+UseG1GC 使用G1垃圾回收器

从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率

G1比CMS的弱项:

Shenandoah收集器

目的是每次停顿都在10ms内

使用了连接矩阵来维护Regin 之间的引用关系:

屏幕截图 2020-10-20 144003

过程:

  1. 初始标记 STW 标记与GC Roots直接关联的对象 停顿时间与GC Roots的数量相关
  2. 并发标记 遍历对象图,标记出全部可达的对象
  3. 最终标记 STW 处理并发标记时产生的新关系
  4. 并发回收 把回收集里面的存活对象先复制一份到其他未被使用的Region之中 使用的读屏障及转发指针实现
  5. 初始引用更新 STW 并发回收阶段复制对象结束后,还需要把堆中所有指 向旧对象的引用修正到复制后的新地址
  6. 并发引用更新
  7. 最终引用更新 STW 解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用
  8. 并发清理

屏幕截图 2020-10-20 145308

转发指针

屏幕截图 2020-10-20 145619

有两点问题需要注意:

  1. 并发更新问题 通过CAS解决
  2. 增加了一层转发肯定会带来效率的损失

ZGC

一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器

目的也是在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟

堆内存布局:

屏幕截图 2020-10-20 150231

过程:

屏幕截图 2020-10-20 151632

  1. 并发标记 遍历对象图做可达性分析的阶段
  2. 并发预备重分配 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
  3. 并发重分配 把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系 如果用户代码此时访问了位于重分配集中的对象 会进行一次引用修改 使其指向新对象
  4. 并发重映射 修正整个堆中指向重分配集中旧对象的所有引用

染色指针

它直接把标记信息记在引用对象的指针上

批注 2020-05-11 100706

由于只有42位作为对象地址 所以ZGC最高能管理的内存只有4TB 并且不支持32位平台 也不支持压缩指针

但是染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理

染色指针的操作系统问题:

重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?

Linux/x86-64平台上的ZGC使用了多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上

屏幕截图 2020-10-20 151432

Epsilon收集器

所谓垃圾收集器 干的不仅仅是收集垃圾的活 它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责

这款垃圾收集器不干GC的活 对于微服务 低内存 运行时间短的应用及时不回收垃圾 也是可以接受的

GC日志分析

一些参数:

  1. 看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc
  2. 看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*
  3. 查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug
  4. 查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+Print-GCApplicationConcurrentTime以及-XX+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog:safepoint
  5. 查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace
  6. 查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution,JDK 9之后使用-Xlog:gc+age=trace

可视化GC日志分析工具

gceasy