0%

垃圾回收

垃圾回收

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

GC主要作用于堆和方法区,其中大多数是在堆中进行

如何判断一个对象是否应该被回收(垃圾标记算法)

既然要进行回收,那么就需要判断一个对象是否是垃圾,是否应该被回收,判断对象是否存活一般有两种方式:引用计数法可达性分析

引用计数法

引用计数法是对每个对象都保存一个整型的引用计数器属性,用于记录对象被引用的情况,对于一个对象,有其他任何一个对象引用它,该对象的引用计数器就加一,当引用失效时,就减一,只要该对象的引用计数器的值为0,则表示该对象已经不再被使用了

优点
  • 实现简单,垃圾对象便于辨识
  • 判断效率高,回收没有延迟性
缺点
  • 需要额外的字段进行存储计数器,需要额外的存储空间开销
  • 每次赋值都需要更新计数器,增加了时间开销
  • 无法处理循环引用的情况,由于这种问题导致java垃圾回收器没有使用该算法

可达性分析

可达性分析是以根对象集合(GC Roots)为起始点,按照从上往下的方式搜索被根对象集合所连接的目标对象是否可达,内存中存活的对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链,如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡

可达性分析可以解决引用计数法中无法处理循环引用的问题

哪些可以作为GC Roots

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,它就是一个Root

  • 虚拟机栈中所引用的对象,如各个线程被调用的方法中使用到的参数、局部变量等
  • 本地方法栈内JNI引用的对象
  • 方法区中类静态属性引用的对象,如类的引用类型静态变量
  • 方法区中常量引用的对象,如字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象
  • java虚拟机内部的引用,如基本数据类型对象的Class对象,一些常驻的异常对象,系统类加载器
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
finalization机制

在Object类中有一个finalize方法

1
protected void finalize() throws Throwable { }

该方法可以被子类进行重写,用于在对象被垃圾回收时进行资源释放,该方法会在垃圾回收该对象之前调用,该方法不要主动的调用

finalize方法只可以被调用一次

虚拟机对象中的状态
  • 可触及的 可达的对象
  • 可复活的 对象的所有引用都被释放,但是对象有可能在finalize方法中复活
  • 不可触及的 finalize方法已经被调用,并且没有复活
判断是否可回收的过程

判断对象是否可回收,至少要经历两次标记过程

  • 如果该对象到GC Roots没有引用链,则进行第一次标记
  • 判断该对象是不是有必要执行finalize方法
    • 如果对象没有重写finalize方法,或者已经被虚拟机调用过,则视为没有必要执行,该对象被判定为不可触及的
    • 如果对象重写了finalize方法,且还没有被调用过,该对象会被插入到F-Queue队列中,由Flializer线程触发其finalize方法
    • finalize方法是对象逃脱被回收的最后机会,GC会对F-Queue队列中的对象进行第二次标记,如果在finalize方法中对象与引用链上的任何一个对象建立了联系,那么在进行第二次标记时,该对象会被移出即将回收的集合。等待下一次没有被引用的时候,但是finalize方法只可以被调用一次

垃圾回收算法

  • 标记-清除

  • 复制

  • 标记-整理

  • 分代收集

标记-清除算法

标记-清除算法(Mark-Sweep)在堆中的有效内存空间被耗尽的时候就会停止整个程序,然后进行两项工作,标记清除

  • 标记 Collector从引用根节点开始遍历,标记所有被引用的对象,在对象头中记录为可达对象
  • 清除 Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其头部中没有标记为可达对象,则将其回收
优点
  • 实现简单
缺点
  • 效率低
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 清理完之后空闲内存不是连续的,产生内存碎片,需要维护一个空闲列表

复制算法

复制算法(Copying)将内存空间分成两块,每次使用其中一块,在垃圾回收时将真正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块内的所有对象,交换两个内存的角色,完成垃圾回收

优点
  • 运行高效
  • 复制过去以后保证空间的连续性,不会出现碎片问题
缺点
  • 需要两倍的内存空间
  • GC需要维护region之间的对象引用关系,不管内存占用或者时间开销都不小

该算法适用于存活对象比较少、垃圾比较多的情况下,所以可以用于新生代中

标记-整理算法

标记-整理算法(Mark-Compact)也是分为了两个阶段,标记整理

  • 标记 该阶段和标记-清除算法一样,从根节点开始标记所有被引用的对象
  • 整理 将所有对象整理到内存的另一端,按顺序排放,然后清理边界外的所有空间
优点
  • 消除了标记-清除算法中内存碎片的缺点,在需要给新对象分配内存时,只需要持有一个内存的起始地址就可以了,不需要维护空闲列表
  • 消除了复制算法中需要两倍内存的缺点
缺点
  • 效率低,比复制算法的效率低,比标记-清除算法效率还低
  • 移动对象的同时,如果对象被其他对象引用,则需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序

分代收集算法

由于没有一个完美的算法,所以根据不同的场景来采用不同的垃圾回收算法,HotSpot根据不同的区域采用不同的垃圾回收算法,也就是分代收集算法

在新生代中,由于新生代中对象生命周期短,存活率低,回收频繁,且新生代中存在两个survivor,所以在新生代使用复制算法,回收速度快

在老年代中,由于老年代区域大,对象生命周期长,存活率高,肯定不可以使用复制算法,所以使用的是标记-清除或者标记-清除和标记-整理混合实现

STW事件(stop the world)

STW事件是指GC发生过程中,会产生应用程序的停顿,停顿产生时整个应用程序线程会被暂停,没有任何响应

在可达性分析算法中枚举根节点(GC Roots)就会导致所有java线程停顿(停顿是由于分析工作必须在一个能确保一致性的快照中进行)

垃圾回收器

有七款经典的垃圾回收器

按照线程数分类

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并发回收期:CMS、G1

按照分代分类

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

可以使用-XX:+PrintCommandLineFlags查看命令行相关参数

也可以使用 jinfo -flag 相关垃圾回收器参数 pid

Serial收集器

Serial收集器是最基本、历史最悠久的收集器,在jdk3之前回收新生代的唯一选择,采用复制算法、串行回收和STW机制的方式执行内存回收,Serial Old收集器是用来执行老年代垃圾收集的收集器,Serial Old收集器采用了标记-压缩算法、串行回收和STW机制的方式执行内存回收

Serial收集器是HotSpot中Client模式下的默认新生代垃圾收集器

Serial Old收集器是HotSpot中Client模式下的默认老年代垃圾收集器

Serial Old收集器在Server模式下的用途

  • 与新生代Parallel Scavenge配合使用
  • 作为老年代CMS收集器的后备垃圾收集方案
优点

简单高效

可以使用-XX:+UseSerialGC参数来指定年轻代使用Serial收集器,老年代使用Serial Old收集器

一般在单核cpu时使用,目前已经不使用串行回收了

ParNew收集器

ParNew(Par是Parallel,New是指新生代)收集器是新生代中的多线程版本,除了使用并行回收外,其他与Serial没有什么区别,也是采用复制算法和STW机制,由于在新生代回收次数较多,所以使用的是并行的方式,而老年代回收次数较少,可以使用串行方式来节省资源,减少了线程切换的消耗。所以老年代可以使用Serial Old收集器来进行回收

可以使用-XX:+UseParNewGC来指定使用ParNew来进行内存回收,只能指定新生代,不影响老年代

同时可以使用 -XX:+UseConcMarkSweepGC来指定老年代使用CMS收集器

使用-XX:ParallelGCThreads来指定线程数量,默认是CPU核数

Parallel Scavenge收集器

Parallel Scavenge收集器采用复制算法、并行回收和STW机制的方式执行内存回收,看上去与ParNew收集器是一样的,但是Parallel Scavenge收集器是一个吞吐量优先的收集器,可以进行自适应调节策略,可以高效的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务,如批量处理、订单处理等,吞吐量优先的场景下使用Parallel Scavenge收集器和Parallel Old收集器进行组合(JDK8中默认的组合)

在jdk6时老年代的默认垃圾收集器由Serial Old收集器变为了Parallel Old收集器

Parallel Old收集器采用了标记-压缩算法、并行回收和STW机制的方式执行内存回收

参数配置
  • -XX:+UseParallelGC 指定新生代使用Parallel并行收集器

  • -XX:+UseParallelOldGC 指定老年代使用并行收集器,开启新生代也会激活老年代,开启老年代也会激活新生代,互相激活

  • -XX:ParallelGCThreads 设置新生代并行收集器的线程数,一般与CPU核数相等,默认情况下,如果CPU核数小于8,则为CPU核数;如果CPU核数大于8,则为3+(5*CPU核数/8)

  • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(STW时间),单位毫秒,与-XX:GCTimeRatio互斥

  • -XX:GCTimeRatio 设置垃圾收集时间占总时间的比例(1/(N+1)) 取值范围为(0,100),默认99

  • -XX:+UseAdaptiveSizePolicy 设置是否开启自适应调节策略,默认开启,如果开启,新生代大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间的平衡点

CMS收集器

jdk5中推出的老年代的并发收集器,尽可能缩短垃圾回收时用户线程的停顿时间,重视服务的响应速度,低延迟,采用的是标记-清除算法和STW机制,该垃圾回收器不能与Parallel Scavenge收集器组合,只能与ParNew或者Serial收集器组合

分为初始标记(会STW)、并发标记(可以与用户线程一起工作,不会产生STW)、重新标记(会STW)、并发清理(可以与用户线程一起工作,不会产生STW)、重置线程(可以与用户线程一起工作,不会产生STW)五个步骤

优点
  • 并发收集
  • 低延迟
缺点
  • 由于使用的是标记-清除算法,所以会产生内存碎片,在无法分配大对象时,只能触发Full GC
  • CMS收集器对CPU资源非常敏感
  • CMS收集器无法处理浮动垃圾,在并发标记阶段产生的新的垃圾,无法进行标记,所以新产生的垃圾不会回收,只能在下一次GC时进行释放
参数配置
  • -XX:+UseConcMarkSweepGC 指定使用CMS收集器,自动触发-XX:+UseParNewGC,使用的组合为ParNew(新生代)+CMS(老年代)+Serial Old(老年代)

  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,达到该阈值,开始进行回收,如果内存增长缓慢,可以设置稍大的值,降低CMS的触发频率,减少老年代回收的次数;如果内存增长迅速,则应该降低该阈值,避免频繁触发Serial Old,有效降低Full GC的执行次数

  • -XX:+UseCMSCompactAtFullCollection 指定在执行Full GC后对内存进行压缩整理,会带来STW

  • -XX:CMSFullGCsBeforeCompaction 设置执行多少次Full GC后对内存空间进行压缩整理

  • -XX:ParallelCMSThreads 设置CMS线程数量,默认为 (ParallelGCThreads + 3) /4,ParallelGCThreads是新生代并行手机器的线程数

jdk9中废弃,jdk14中删除

G1收集器

分区回收

jdk7引入G1收集器,G1是一个并发收集器,将堆内存分割为很多不相关的区域(region),使用不同的region来表示Eden、Survivor0、Survivor1、老年代等,避免在整个堆中进行全区域的垃圾收集,跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小比上回收所需的时间),维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

面向服务端应用,针对大内存、多处理器的机器使用G1收集器

垃圾回收过程
  • 新生代GC(Young GC)

    应用程序分配内存,当新生代的Eden区用尽时开始新生代回收过程,G1新生代收集阶段是一个并行的独占式收集器,新生代回收期,G1会暂停所有应用程序线程,启动多线程执行新生代回收,然后从新生代移动存活对象到Survivor区间或者老年区间,新生代垃圾回收只会回收Eden区和Survivor区,会触发STW

  • 老年代并发标记(Concurrent Marking)

    当堆内存达到一定值时(默认45%,-XX:InitiatingHeapOccupancyParcent),开始老年代并发标记过程

  • 混合回收(Mixed GC)

    标记完成之后立即开始混合回收过程,G1从老年区间移动存活对象到空闲区间,这些空闲区间就变成可老年代的一部分,和新生代不同,老年代的G1回收器不需要整个老年代回收,一次只需扫描一小部分老年代的region,同时,这个老年代的region是和新生代一起被回收的

  • Full GC(单线程、独占式、高强度的Full GC 作为保底方案)

优点
  • 并行与并发 可以多个GC线程同时工作,不过会导致STW;可以与应用程序交替执行,部分工作可以与应用程序同时执行
  • 分代收集 G1依然属于分代回收,不过不要求Eden、新生代或者老年代是年需的,而是将堆空间分为若干个region,这些region中包含了逻辑上的新生代和老年代
  • 空间整合 G1将内存划分为一个个的region,内存回收是以region为基本单位的,region之间是复制算法,整体上是标记-整理算法,可以避免内存碎片
  • 可预测的停顿时间模型 避免在整个堆中进行全区域的垃圾收集,跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小比上回收所需的时间),维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限时间内可以获取尽可能高的收集效率
缺点

G1需要更高的内存占用,所以在小内存的应用上CMS的表现会优于G1,而在大内存的应用上G1可以有很大的优势(6~8G)

参数配置
  • -XX:+UseG1GC 指定使用G1收集器
  • -XX:G1HeapRegionSize 设置每个region的大小,值是2的次幂,范围是1M到32M之前,目标是根据最小的堆大小划分出2048个region,默认是堆内存的1/2000
  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(STW时间),不能保证一定可以达到,但是会尽量保证,默认200ms
  • -XX:ParallelGCThread 设置并行垃圾回收线程数,最大为8
  • -XX:ConcGCThreads 设置并发标记的线程数,ParallelGCThread`的1/4左右
  • -XX:InitiatingHeapOccupancyParcent 触发并发GC周期的堆占用率阈值,超过则会触发GC,默认45

jdk9中为默认垃圾收集器

在使用G1垃圾收集器时,尽量避免使用-Xmn-XX:NewRatio等选项显式设置年轻代大小,固定的年轻代大小会覆盖暂停是啊金目标

评估GC的性能指标

  • ※吞吐量:运行用户代码的时间占总运行时间的比例
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
  • ※暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(STW时间),越小越好
  • 收集频率:相对于应用程序的执行,收集操作发生的频率
  • 内存占用:java堆区所占用的内存大小
  • 快速:一个对象从诞生到被回收所经历的时间

几种垃圾收集器对比

垃圾收集器对比

Java垃圾回收机制最基本的做法是分代收集。内存中的区域被分为不同的年代,对象根据其存活的时间被保存在对应年代的区域内。

分为3个年代:新生代、老年代和元空间。

内存的分配发生在新生代中,当对象存活时间足够长时,会被复制到老年代。

垃圾回收根据回收区域不同分为两种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

部分收集Partial GC

部分收集不是完整收集整个堆的垃圾回收

  • Minor GC新生代收集 :只对新生代进行垃圾回收,Eden满的话会触发Minor GC,Survicor不会去触发,只不过Minor GC也会去清理Survicor
  • Major GC老年代收集 :只对老年代进行垃圾回收(只有CMS GC会单独对老年代进行垃圾回收)
  • Mixed GC混合收集:对整个新生代以及部分老年代进行垃圾回收(只有G1 GC会存在Mixed GC)

整堆收集Full GC

整堆收集会收集整个java堆和方法区的垃圾收集

GC日志显式参数配置

  • -XX:+PrintGC 输出GC日志
  • -XX:+PrintGCDetails 输出GC的详细日志
  • -XX:+PrintGCTimeStamps 输出GC的时间戳
  • -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式)
  • -XX:+PrintHeapAtGC 在进行GC前后打印出堆的信息
  • -Xloggc:../logs/gc.log 日志文件输出路径,可以使用gc分析工具,如gcViewer、gcEasy(gceasy.io)