目录

JVM-垃圾回收

为什么要垃圾回收

如果不进行垃圾收集,内存数据很快就会被占满

什么是垃圾

没有被引用的对象就是垃圾

如何找到垃圾

/images/jvm/jvm-gc-find.png

引用计数算法(Reference Counting)

  • 当这个对象引用都消失了,消失一个计数减一,当引用都消失了,计数就会变为0。此时这个对象就会变成垃圾。
  • 在堆内存中主要的引用关系有如下三种:单一引用、循环引用、无引用
  • 引用计数算法不能解决循环引用问题。为了解决这个问题,JVM使用了根可达分析算法。

根可达算法(GCRoots Tracing)

又叫根搜索算法。在主流的商用程序语言中(Java和C#),都是使用根搜索算法判定对象是否存活的。 基本思路就是通过一系列的名为“GCRoot”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GCRoot没有任何引用链相连时,则证明此对象是不可用的,也就是不可达的。

可作GCRoots的对象

  • 虚拟机栈中,栈帧的本地变量表引用的对象。
  • 方法区中,类静态属性引用的对象。
  • 方法区中,常量引用的对象。
  • 本地方法栈中,JNl引用的对象。

回收过程

即使在可达性分析算法中不可达的对象,也并非是“非死不可”。被判定不可达的对象处于“缓刑”阶段。 要真正宣告死亡,至少要经历两次标记过程:

  • 第一次标记:如果对象可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标 记;
  • 第二次标记:第一次标记后,接着会进行一次筛选。筛选条件:此对象是否有必要执行finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果失败则继续存活

对象引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱。

/images/jvm/jvm-gc-reference.png

如何清除垃圾

标记清除算法(Mark-Sweep)

/images/jvm/jvm-gc-Mark-Sweep.png

最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

缺点

  • 效率不高,标记和清除过程的效率都不高
  • 空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC 。

复制算法(Copying)

/images/jvm/jvm-gc-Copying.png

  • 为解决效率。它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 现在商业虚拟机都是采用这种收集算法来回收新生代,当回收时,将Eden和Survivor中还存活着的对象 拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

优缺点

  • 优点:没有碎片化,所有的有用的空间都连接在一起,所有的空闲空间都连接在一起
  • 缺点:存在空间浪费

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

/images/jvm/jvm-gc-MC.png

  • 标记:标记出所有需要回收对象
  • 清除:统一回收掉所有对象
  • 整理:将所有存活对象向一端移动

老年代没有人担保,不能用复制回收算法。可以用标记-整理算法,标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

优缺点

  • 优点:空间没有浪费,没有内存碎片化问题
  • 缺点:性能较低,因为除了拷贝对象以外,还需要对象内存空间进行压缩,所以性能较低。

分代回收(Generational Collection)

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

  • 新生代,每次垃圾回收都有大量对象失去,选择复制算法,弱分代假说
  • 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法,**强分代假说 **

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载,在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

用什么清除垃圾

有 8 种不同的垃圾回收器,它们分别用于不同分代的垃圾回收。

  • 新生代(复制算法):Serial,ParNew,Parallel Scavenge
  • 老年代(标记-清除、标记-整理):SerialOld,Parallel Old,CMS
  • 整堆回收器:G1、ZGC

/images/jvm/gc.png
垃圾收集器

串行收集器 Serial与SerialOld

配置参数: -XX:+UseSerialGC

特点

  • Serial新生代收集器,单线程执行,使用复制算法
  • SerialOld老年代收集器,单线程执行,使用标记-整理算法
  • 进行垃圾收集时,必须暂停用户线程
  • 对于单个CPU的环境来说,Serial收集器由于没有线程交互的开销,收集效率更高。

/images/jvm/jvm-gc-Serial.png

并行收集器

Parallel Scavenge收集器

  • 配置参数: -XX:+UseParallelGC,目标是达到一个可控制的吞吐量(Throughput)

  • 吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

  • 吞吐量优先收集器,垃圾收集需要暂停用户线程

  • 新生代使用并行回收器,采用复制算法

  • 老年代使用串行收集器,采用标记-整理算法

/images/jvm/jvm-gc-ps.png

Parallel Old收集器

  • 配置参数: -XX:+UseParallelOldGC
  • Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  • 在注重吞吐量,CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
  • 老年代使用并行收集器,采用标记-整理算法

/images/jvm/jvm-gc-pso.png

ParNew收集器

  • 配置参数: -XX:+UseParNewGC
  • 配置参数: -XX:ParallelGCThreads=n 设置并行收集器收集时使用的并行收集线程数。一般最好和计算机的CPU相当
  • 新生代并行(ParNew),老年代串行(Serial Old)
  • Serial收集器的多线程版本
  • 注意:单CPU性能并不如Serial,因为存在线程交互的开销

/images/jvm/jvm-gc-ParNew.png

CMS收集器

配置参数: -XX:+UseConcMarkSweepGC 应用CMS收集器。

特点:

  • 低延迟:减少STW对用户体验的影响【低延迟要求高】
  • 并发收集:可以同时执行用户线程
  • CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收。
  • CMS收集器的垃圾收集算法采用的是标记-清除算法
  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。
  • CMS收集器对CPU资源非常敏感。

/images/jvm/jvm-gc-CMS.png

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段:

  • 初始标记(Initial-Mark)阶段:
    • 本阶段任务:标记出GCRoots能直接关联到的对象。
    • 一旦标记完成之后就会恢复之前被暂停的所有应用线程。
    • 由于直接关联对象比较小,所以这里的速度非常快。
    • 会STW
  • 并发标记(Concurrent-Mark)阶段:
    • 本阶段任务:从GC Roots的直接关联对象遍历整个对象图
    • 这个过程耗时较长
    • 不会STW
  • 重新标记(Remark)阶段:
    • 本阶段任务:修正并发标记期间,因用户程序继续运作产生的新的对象记录
    • 这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
    • 会STW
  • 并发清除(Concurrent-Sweep)阶段:
    • 本阶段任务:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。
    • 由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

G1(Garbage-First)收集器

G1是一款面向服务端应用的全功能型垃圾收集器,大内存企业配置的主要是G1。 配置参数: -XX:+UseG1GC

  • 吞吐量和低延时都行的整堆垃圾收集器
  • G1最大堆内存32M2048=64GB,最小堆内存1M2048=2GB,低于此值不建议使用
  • 全局使用标记-整理算法收集,局部采用复制算法收集
  • 可预测的停顿:能让使用者指定GC消耗时间,默认是200ms

/images/jvm/jvm-gc-G1.png

G1收集器的运作大致可划分为以下几个步骤:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
  2. 并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
  3. 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

G1中有三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。

G1内存划分

G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了新生代、老年代的物理划分,取而代之 的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的新生代、老年代区域。

好处:不用单独的空间对每个代进行设置,不用考虑每个代内存如何分配。

/images/jvm/jvm-gc-G1-memory.png

局部采用复制算法:

  • G1新生代垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间
  • G1通过将对象从一个区域复制到另外一个区域,完成了清理工作。 相当于在正常的处理过程中,
  • G1完成了堆的压缩,这样就不会有cms内存碎片问题了

Humongous区域:在G1中,有一种特殊的区域叫Humongous区域

  • 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。 这些巨型对象,默认直接会被分配在老年代。
  • 但是,如果是一个短期存在的巨型对象,在分区之间来回拷贝,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。

收集器相关参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-XX:+UseG1GC
# 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=
# 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是 200 毫秒。
-XX:G1HeapRegionSize=n
# 设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。
# 目标是根据最小的 Java 堆大小划分出约 2048 个区域。
# 默认是堆内存的1/2000。
-XX:ParallelGCThreads=n
# 设置并行垃圾回收线程数,一般将n的值设置为逻辑处理器的数量,建议最多为8。
-XX:ConcGCThreads=n
# 设置并行标记的线程数。将n设置为ParallelGCThreads的1/4左右。
-XX:InitiatingHeapOccupancyPercent=n
# 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

ZGC(Z Garbage Collector)

ZGC ( Z Garbage Collector )在 JDK11中引入的一种可扩展的低延迟垃圾收集器,在 JDK15 中发布稳定版。 配置参数: -XX:+UseZGC 特点:

  • < 1ms 最大暂停时间(JDK16-是10ms,JDK16+是<1ms ),不会随着堆内存增加而增加
  • 适用内存大小从 8MB 到16TB 的堆
  • 并发,基于Region,压缩,NUMA感知,使用色彩指针,使用负载屏障
  • 垃圾收集算法:标记-整理算法
  • 主要目标:低延时

收集器相关参数

1
2
3
4
-XX:+UseZGC # 启用 ZGC
-Xmx # 设置最大堆内存
-Xlog:gc # 打印 GC日志
-Xlog:gc* # 打印 GC 详细日志

垃圾收集器对比

/images/jvm/gc-vs.jpg
垃圾收集器对比

Minor GC 、Major GC和 Full GC 有什么区别

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。Minor GC 非常频繁,回收速度比较快。

  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集, Major GC 一般比 Minor GC慢 10 倍以上。

    • 目前,只有 CMS GC 会有单独收集老年代的行为
    • 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。