Java垃圾回收器

一、什么是GC,为什么要GC

垃圾回收机制是java的招牌能力,极大的提高了开发效率,如今,垃圾回收几乎成为现代语言的标配,即使经过如此长时间的发展,java的垃圾回收机制仍然在不断的演进中,不同大小的设备,不同特征的应用场景,对垃圾回收提出了新的跳转。

垃圾回收只发生于堆和方法区中

二、垃圾标记阶段

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存货对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放其占用的内存空间,因此这个过程被称为标记阶段

判断对象存活一般有两种方式:引用计算法和可达性分析算法

引用计数算法

描述:对每个对象保存一个整数类型的引用计数器属性,用于记录对象被引用的情况,对象每次被引用时数值加一,当没有对象引用它时,则被认为是垃圾

优点: 实现简单,垃圾对象辨识效率高,回收没有延迟性。

缺点: 需要单独的字段存储计数器,增加了内存开销。

​ 对象每次复制都要更新计数器,增加了时间开销。

无法处理循环引用的情况,这是致命缺陷导致java没有使用此类算法

循环引用:假设 p引用了对象a,a引用了b,b引用了c,c又引用了a,此时a被引用的次数为2( p和c的引用),b引用次数为1,c的引用次数为1,一切正常。当时当p不在引用a时,a的引用次数变为1所以a不会被回收,b,c由于被a引用也不会被回收,而这三个已经是无用的垃圾了。

在这里插入图片描述

java考虑到了引用计数算法不能解决循环依赖的问题,没有使用引用计数算法。像Python就使用的是此类算法

Python是如何解决循环引用问题的?

  1. 手动解除,在合适的实际,接触互相引用的关系。
  2. 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环依赖问题。

可达性分析算法

描述:以根对象集合(GC Roots)为起始点,搜索被根对象直接或间接引用的对象,这个路径称作引用链,如果目标对象没有被任何引用链连接,则视为不可达对象,会将其标记为垃圾。

GC Roots包括以下几类元素:

  • 虚拟机栈中引用的对象:比如各个方法中的参数,局部变量等。
  • 本地方法栈内JNI(本地方法)引用的对象。
  • 方法区中类静态属性引用的对象。
  • 所有被同步锁(synchronize)持有的对象。
  • Java虚拟机内部的引用。

可达性分析算法在判断内存是否可回收时必须在一个能保证一致性的快照中进行,否则分析结果的准确性无法得到保证。因此java在进行GC时必须"Stop The World"(停止用户线程)的一个重要原因。

即使号称不会发送停顿的CMS收集中,枚举根节点时也是必须要停顿的

finalize

java提供了对象终止机制(finalization)来允许开发人员自定义对象销毁之前的处理逻辑。垃圾回收器在回收对象之前会先调用对象的finalize()方法

finalize()是Object类中的方法,允许重写,用于在对象被回收时释放资源,通常进行一些资源释放和清理工作,如关闭文件,stock和数据库连接等。

永远不要主动调用对象的finalize() 方法,应交由垃圾回收器调用,因为:

  • 在调用finalize()方法时可能导致对象复活。
  • finalize()的执行时机是没有保证的,如果不发送GC则不会执行。
  • 一个糟糕的finalize()方法严重影响GC性能。

判断一个对象是否可回收时,会进行2次标记

  1. 如果对象到GCRoots没有引用链,则进行第一次标记
  2. 进行筛选,判断对象是否要执行 finalize() 方法
    • 如果没有重写finalize()方法,或者finalize()方法已经被调用过,则虚拟机不调用finalize()方法,直接标记为垃圾
    • 如果重写了finalize()方法且未被执行,那么对象会被放入F-Queue队列中,由虚拟机自动创建的低优先级的finalizer线程执行finalize()方法。
    • finalize()方法是对象逃脱死亡的唯一机会,在进行finalize()方法执行是,如果与其他对象建立了联系,那么该对象将会被移出清除队列,但是由于finalize()方法只会被调用一次,也就是说对象只能逃逸一次,下次就不会逃逸了。

java9已经废弃了finalize()方法

三、垃圾回收阶段

标记-清除算法 Mark-Sweep

标记—清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。

标记: Collector从引用根结点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

优点

  • 实现简单

缺点

  • 效率不算高
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表

复制算法

概念: 将内存分为2个区域,每次只使用其中的一块,在垃圾回收时遍历使用中的内存数据,将有用的数据复制到另一块内存中,然后清除剩余的垃圾对象。

优点:

没有标记和清除的过程,效率高。复制到新的内存可以保证内存地址是连续的,不会出现碎片问题。

缺点:

将内存分为2块,较耗内存,迁移对象时比较好使,如果内存里的大部分都是有用的性能就比较慢了。

新生代所用的算法就是复制算法,垃圾对象多。

标记-压缩算法 Mark-Compact

标记压缩算法是标记清除算法的优化

标记: Collector从引用根结点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

压缩: 回收垃圾对象,移动剩余对象使其内存地址连续

优点:

解决了 标记-清除算法中内存区域分散 和 复制算法内存浪费的缺点。

缺点:

效率低于复制算法和标记清除算法。

标记清除标记压缩复制
速度较快较慢
空间开销需要2倍内存
内存连续否(会出现碎片)

分带收集算法

目前几乎所有的GC都是采用分带回收算法进行垃圾回收

GC所使用的算法必须结合年轻带和老年代各自的特点

新生代(YoungGen): 内存占用小,对象生命周期短,垃圾多,回收频繁。

​ 这种情况使用复制算法速度是最快的,并且新生代本身内存占用就小,复制算法不会浪费太多内存

老年代(TenuredGen): 内存占用大,对象生命周期长,存活率高,不需要频繁回收。

​ 这种情况混合使用标记清除和标记整理比较合理。

增量收集

GC线程和用户线程来回切换,每次清理一点区域后立即切换回用户线程执行,减小停顿时间

分区算法

在相同条件下,堆空间越大,进行一次GC耗费的时间就越长,停顿的也月长,为了更好控制GC时停顿的时间,我们可以将一块大的内存空间划分为若干个小块,每次合理地回收若干个小块,以减小GC所产生停顿的时间

G1回收器的思想

四、垃圾回收器

垃圾回收器发展历史:

1999年JDK1.3.1发布了串行方式的SerialGC,是第一款垃圾回收器,ParNew垃圾回收器是SerialGC的多线程版本

2002年JDK1.4发布了 ParallelGC和Cuncurrent Mark Sweep垃圾回收器,ParallelGC成为Hotspot默认的垃圾回收器

2012年 JDK1.7发布了G1垃圾回收器

2017年 JDK9将G1变成默认的垃圾回收期以替代CMS回收器

2018年JDK10改进G1垃圾回收器

2018年9月JDK11引入Epsilon垃圾回收器,又被称为“No-Op”(无操作)回收器,同时引入可伸缩低延迟垃圾回收器ZGC

2019年3月JDK12增强G1,引入ShenandoahGC

2019年9月JDK13发布增强ZGC,自动返回未用堆内存给操作系统

2020年3月JDK14发布,删除CMS垃圾回收器,扩展ZGC在MacOS和Windows上的引用

按执行方式分类:

  • 串行回收器:SerialGC,SerialOldGC
  • 并行回收器:ParNewGC,Parallel ScavengeGC,Parallel OldGC
  • 并发回收器: CMSGC,G1GC

按回收区域分类:

  • 新生代: SerialGC,ParNewGC,Parallel ScavengeGC
  • 老年代:Serial OldGC,Parallel Old GC,CMSGC
  • 整个堆:G1GC

查看当前系统使用的默认垃圾回收器:

  • 使JVM命令行参数 -XX:+PrintCommandLineFlags
  • 使用指令 jinfo -flag

搭配使用情况

在这里插入图片描述

Serial GC和Serial Old GC

Serial收集器是最基本,历史最悠久的垃圾回收器,采用复制算法,串行回收和“Stop the World”机制

Serial Old同样也采用串行回收和“Stop the World”机制,回收的是老年代的垃圾,使用的是标记压缩算法

优点: 对于单CPU环境来说,Serial收集器没有线程交互的开销,专心做垃圾收集,效率很高。适合用在小型(单核CPU,几十MB,几百MB)的应用。

使用: -XX:+UseSerialGC 即可使用 SerialGC和Serial Old GC

ParNewGC

ParNewGC是SerialGC的多线程版本,采用复制算法,并行回收和“Stop the World”机制 采用复制算法,回收新生代垃圾

ParNewGC虽然是SerialGC的多线程版本,但是在单CPU情况下效率并不比SerialGC高,只在多核CPU时效率比SerialGC高。

使用: 通过 -XX:+UseParNewGC 使用ParNew回收器

Parallel GC和Parallel Old GC

Parallel GC同样采用复制算法,并行回收和“Stop the World”机制,性能和ParNew相差不多,主要目的是达到一个可控的吞吐量,被称为吞吐量优先的垃圾回收器。自适应调节也是ParallelGC和ParNew的一个重要区别

Parallel Old GC采用标记压缩算法,并行回收和“Stop the World”机制,收集老年代的垃圾。

高吞吐量可以高效利用CPU,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务,因此常在服务器端使用,如执行批量任务,订单处理,工资支付等应用程序。

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

比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

JDK8默认使用 Parallel GC

参数

-XX:+UseParallelGC 开启ParallelGC

-XX:+UseParalleOldlGC 开启ParallelOldGC 注意两者是相互激活的,开启一个另一个自动开启

-XX:ParallelGCThreads=8 使用设置年轻代并行回收器的线程数,一般与CPU相等,避免过多线程影响性能。默认情况下8核以下,该值等于CPU个数,超过8核默认值为 3+(5*cpu个数/8)

-XX:MaxGCPauseMillis=20 设置垃圾回收器最大停顿时间(STW),单位是毫秒。慎用,因为会调整Java堆大小或其他参数

-XX:GCTimeRatio=n 垃圾回收时间占总时间的比例用于平衡吞吐量的大小( 1/(n+1) ),取值范围是(0,100)默认值是99,也就是回收时间占时间不超过1%。

-XX:+UseAdaptiveSizePolicy 开启ParallelGC的自动调节策略,这种情况下会调整新生代Eden,Survivor的比例和老年代阈值,已达到吞吐量和停顿时间的平衡。

Concurrent Mark Sweep

CMS GC是第一款真正意义上的并发收集器,第一次实现了让垃圾回收线程与用户线程同时工作,用于回收老年代

CMS关注的是尽可能缩短垃圾收集时用户线程停顿的时间,比较适合用于B/S架构。

CMS使用的是标记清除算法,同样存在StopTheWorld

CMS与其他垃圾收集器不同,在内存达到设置阈值就会进行垃圾回收

CMS GC工作原理

在这里插入图片描述

CMS整个过程较为复杂,主要分为4个阶段: 初始标记,并发标记,重新标记,并发清理

初始标记:程序所有的工作线程短暂暂停,标记GCRoots能直接关联到的对象(标记存活对象),一旦标记完成就会恢复所有被暂停的进程,这个阶段执行速度非常快。

并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但不会停止用户线程,并发处理。

重新标记:由于在并发标记阶段中,程序的工作线程和垃圾回收线程同时运行,可能会出现标记不准的情况,所以会停止所有线程来进行重新标记。

**并发清理:**清理掉标记阶段已经死亡的对象,释放内存空间。由于不需要移动存活对象,因此可以并发执行。

CMS的优缺点:

优点:

​ 并发收集,低延迟:由于最耗时的并发标记和并发清理阶段不需要停止用户线程,所以整体的回收是低停顿的。

缺点:

​ 会产生内存垃圾:由于并发收集只能采用标记清除算法会出现内存碎片,在无法分配大对象的情况下提前触发FullGC

​ 对CPU资源敏感:虽然不会导致用户停顿,但是会占用一部分线程导致应用程序变慢,总吞吐量变低。

​ 无法处理浮动垃圾:在二次标记时无法处理那些已经标记为非垃圾对象但第二次变为垃圾对象的垃圾。

参数

-XX:+UseConcMarkSweepGC : 开启CMS,会自动打开 ParNew+CMS+Serial Old组合

-XX:CMSSlnitiatingOccupanyFaction=92 设置开启CMSGC的阈值,一旦内存占用到达指定百分比后开启垃圾回收,1.5为68%,1.6以后为92%

-XX:+UseCMSCompactAtFullCollection: 开启压缩整理功能,到达指定次数后进行一次压缩

-XX:CMSFullGCsBeforeComaction: 设置执行多少次GC后进行压缩整理

-XX:ParallelCMSThreads=n 设置CMS的线程数 默认为(n+3)/4

如何选择垃圾回收器

如果微小应用(单核CPU,内存小)选择SerialGC

如果追求应用程序的吞吐量选择ParallelGC

如果追求应用程序的低延迟请选择CMSGC

Garbage First(G1)

概况:再低延迟的情况下获得尽可能高的吞吐量,采用并行回收,将堆分为很多不相干的Region,不同的Region存放部分Eden,S0,S1,老年代等的数据。

G1有规划的避免进行整堆回收,跟踪每个Region的垃圾堆积价值大小(回收所获得的空间大小及回收所需要时间),在后台维护优先列表,优先收集那些耗时短且垃圾最多的Region

由于重点在于回收垃圾最大的区域,所以名字叫G1 垃圾优先

特点:

并行与并发

​ 并行性:G1在回收时会启动多个线程进行回收,有效利用多核计算能力

​ 并发性:G1在回收时可以和用户线程同时执行,不会再整个回收阶段出现阻塞问题

分代收集:G1将内存分为很多的Region,在回收时只回收部分价值高的Region,同时兼顾新生代和老年代

**空间整理:**G1以Region作为基本单位进行垃圾回收,即使采用的是标记清除算法也能避免内存碎片,分配大对象时不会因无法得到连续的内存空间二提前触发GC。

不足

G1不具备全方位压倒性优势,比如在用户程序中,G1无论是在内存的占用还是在程序运行时额外执行的负载都要比CMS高。6-8GB之间 G1和CMS性能差不多,8GB以上G1性能高于CMS

常用参数

-XX:+UseG1GC 使用G1收集器

-XX:G1HeapRegionSize=N 设置每个Region的大小,值是2的幂,访问是1MB到32MB之间,目的是将堆划分为2048个区域,默认是堆内存的1/2000

-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间(默认为200 ms)

-XX:ParallGCThread 设置STW时回收线程数 最大为8

-XX:ConcGCThreads 设置并发标记的线程数建议为ParallGCThread/4

-XX:InitiatingHeapOccupancyPercent 设置触发GC的阈值,内存达到百分之多少进行一次GC

G1的设计原理就是简化JVM性能调优,开发人员一般只需要3步即可完成调优,

1: 开启G1垃圾收集器 -XX:+UseG1GC

2:设置堆最大内存 -Xms8G

3:设置最大停顿时间 -XX:MaxGCPauseMillis

G1提供了三种回收模式:YoungGC,MixedGC,Full GC

Regin:一个Regin可能存储的是Eden,Survivor或者Old的内存区域且只能是其中的一个。Regin存储的区域是可变的,这次存的是新生代空间,当Regin内没有存放数据后会被放入空闲队列,重新分配内存区域时可能就不是新生代的空间了。

Humongous:当一个大对象的大小超过1.5个regin时会被放到Humongous区域,因为大对象会直接分配到老年代,如果临时的对象放入了老年代就会造成内存回收不及时(老年代满了才回收垃圾)因此分配了Humongous存放大数据。

回收过程

  • 年轻代回收 YoungGC
  • 老年代并发标记 ConcurrentMarking
  • 混合回收 MixedGC
  • 如果需要,单线程,独占式,高强度的FullGC还是继续存在的,主要针对GC提供了一种保护机制。

当年轻代的Eden区用尽时开始YoungGC,YongGC工作时是并行独占式的,会停止所以用户线程,因为涉及到将对象移动到S区或老年代,内存地址会发生变动。

当堆内存达到指定的阈值(默认45%)时,开始并发标记标记每个region的活性(存活对象占比),如果一个region的活性为0(全是垃圾)会被立即回收。

老年代并发标记完成后会进行短暂的STW,重新标记不是垃圾的对象,重新标记完后马上开始并发混合回收,OldGC会将老年代Region内存活对象移动到空闲的Regin内,然后清理掉剩下的Region将region放到空闲列表内。

Remembered Set

每一个Region都维护一个RememberedSet集合记录着当前Regin内被其他Regin引用的情况,每次进行引用对象是会创建写屏障判断 对象和引用的对象是否在同一个region中,如果不在同个Region则记录到RememberedSet中。

为什么G1需要为每一个Regin维护一个RememberedSet?

因为G1将内存分成了一个个Region,导致对象分散在各个Regin中,每次回收只回收部分Regin,但是这个Region内的对象可能被其他Region内的对象引用,如果我们不维护RememberedSet,那么在进行回收时就不得不遍历所有的Region才能判断Regin内的对象是不是垃圾,有个RememberedSet记录应用情况就不会发送全堆扫描。

总结

垃圾收集器运行方式回收区域使用算法特点适用场景
Serial串行新生代复制算法响应速度快使用与单核CPU,小内存
Serial Old串行老年代标记压缩响应速度快使用与单核CPU,小内存
ParNew并行新生代复制算法响应速度快多CPU下与CMS合作
Parallel并行新生代复制算法吞吐量优先后台运算
Parallel Old并行老年代标记压缩吞吐量优先后台运算
CMS并发,并行老年代标记清除响应速度优先B/S架构
G1并发,并行新生代,老年代复制,标记压缩,分区响应速度优先服务端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值