jvm-垃圾回收

jvm-垃圾回收

丁起男 5 2022-11-25

jvm-垃圾回收

垃圾是指在运行程序中没有任何指针指向的对象

如果不即使清理垃圾,那么这些垃圾会一直保存到程序结束,空间无法被其他对象使用,可能会导致内存溢出

相关算法

标记阶段

在堆里存放着几乎所有的java对象,在gc指向垃圾回收之前,首先需要区分出内存中哪些存活哪些死亡。只有被标记为死亡的对象,才会在执行垃圾回收时,是否掉其所占用的内存空间,因此这个过程我们称之为垃圾标记阶段

当一个对象已经不再被任何的存活对象引用时,就可以宣判死亡

引用计数算法

对每对象都保存一个整形的引用计数器属性。用于纪录对象被引用的情况

如果一个对象计数器为0,即表示对象不再被使用,可以被回收

优点:

  • 实现简单,垃圾对象便于辨识

  • 判定效率高,回收没有延迟性

缺点:

  • 需要单独的字段存储计数器,增加了额外的存储空间的开心
  • 每次赋值都需要更新计数器,伴随着加法和减肥操作,增加了时间开销
  • 无法处理循环引用的情况(导致了java没有使用这种方式)

python就是使用的引用即使算法,python是如何解决循环引用的?

  • 手动解除,就是在合适的时机,解除引用关系
  • 使用弱引用weakref,weakref是python的标准库,旨在解决循环引用

可达性分析算法

也叫根搜索算法、追踪性垃圾收集

可达性分析算法不仅具备实现简单和执行高效等特定,还可以有效解决循环引用的问题

所谓”gc roots“就是一组必须活跃的引用

基本思路:

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

gc roots包括:

  • 虚拟机栈中引用的对象
  • 本地方法栈内jni引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁synchronized持有的对象
  • jvm内部的引用
  • 反应jvm内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

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

如果要使用可达性分析算法来判断内存是否能回收,那么分析工作必须在一个能保障一致性的快照中进行。这点也是导致gc进行时必须”stop the world“的一个重要原因

清除阶段

当成功区分出存活对象和死亡对象后,gc接下来的任务就是执行垃圾回收,释放掉占用的内存空间

算法 标记-清除 标记-压缩 复制
速度 中等
空间开销
是否有碎片
是否移动对象

标记-清除算法

当堆中的有效空间被耗尽时,就会停止整个程序(stw),然后进行两项工作:

  1. 标记:collector从根节点开始遍历,标记所有被引用的对象(非垃圾)。一般是在对象的header中纪录为可达对象
  2. 清除:collector对堆内存从头到尾进行线性遍历,如果发现某个对象在header中没有标记为可达对象,则将其回收

缺点:

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

注意:这里所谓的清除不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间释放够,如果够,就存放

复制算法

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

优点:

  • 没有标记和清除的过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题

缺点:

  • 此算法的缺点也是很明显的,就是需要两倍的内存空间
  • 对于g1这种拆分成大量region(区)的gc,复制而不是移动,意味着gc需要维护region之间对象引用关系,不管是内存占用还是时间开销都不小

如果垃圾对象不是很多,存活的对象很多,效果会很不理想。所以一般用在新生代

标记-压缩(整理)算法

复制算法不适应于老年代这样大部分对象都是存活的情况,标记-清除算法不仅效率低下,而且在执行完成后会产生大量的内存碎片,所以jvm的设计者需要在此基础上进行改进,标记-压缩算法就此诞生

执行过程:

  1. 和标记清除算法一样,从根节点出发标记所有被引用的对象
  2. 将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外的所有空间

标记-压缩算法不需要维护空闲列表

优点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可
  • 消除了复制算法当中,内存减半的高额代价

缺点:

  • 效率最低
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序(stw)

分代收集算法

分代收集算法:是基于这样的一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率

目前几乎所有的gc都是采用分代收集算法来执行垃圾回收的

  • 年轻代:区域相对较小,对象生命周期短,存活率低,回收频繁。这种情况复制算法的回收整理,速度最快。复制算法的效率只和当前存活对象大小相关,因此很适合年轻代。而复制算法中内存利用率不高的问题也通过两个survivor的设计解决
  • 老年代:区域较大,对象生命周期长、存活效率高,回收不及年轻代频繁。这种存在大量存活率高对象,适合标记-清除算法和标记-整理算法混合实现

以hotspot的cms为例:

  • cms是基于标记-清除算法实现的,对于对象的回收效率很高
  • cms采用基于标记-整理算法的serial old回收器作为补偿措施;当内存回收效率不佳(碎片导致concurrent mode failure时),采用serial old执行full gc以达到堆老年代内存的整理

增量收集算法

如果一次性将所有的垃圾进行处理,需要造成系统长时间停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成

总的来说,增量收集算法的基础仍然是传统的标记-清除和复制算法。增量收集算法通过堆线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记-清理或复制工作

缺点:

使用这种方式,由于在垃圾回收过程中,间断性的还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成吞吐量下降

分区算法

一般来说,相同条件下,堆空间越大,一次gc时所需要的时间就越长,有关gc产生的停顿也越长。为了更好的控制gc的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次gc所产生的停顿

分代算法按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region

每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小空间

System.gc()

默认情况下,通过System.gc()活着Runtime.getRuntime().get()的调用,会显示触发full gc

然而System.gc()调用附带一个免责声明,无法保证堆垃圾收集器的调用。一般情况下垃圾回收应该是自动进行的

如果想要强制调用finalize,可以使用System.runFinalization()

内存溢出与泄露

内存溢出

  • 内存溢出相对于内存泄露来说更容易被理解,但是同样的也是引发程序崩溃的罪魁祸首之一
  • 由于gc一直在发展,一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收速度跟不上内存消耗的速度,否则不容易出现oom情况
  • 大多数情况下,gc会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的full gc操作,这时候回收大量的内存,供应用程序继续使用
  • javadoc堆oom的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存

没有空闲内存的情况,说明jvm堆内存不够,原因:

  • jvm的堆内存设置不够
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集

如果分配一个超大对象,jvm判断垃圾收集不能解决问题,直接抛出oom

内存泄露

  • 只有对象不再被程序用到了,但gc又不能回收他们时,才叫内存泄露
  • 宽泛意义上一些对象的生命周期过长,甚至导致oom,也可以叫做内存泄露

尽管内存泄露不会立即引起程序崩溃,但是一旦发生内存泄露,可能会逐步蚕食内存,导致oom

这里的内存是指虚拟内存,而不是物理内存

常见内存泄露:

  • 单例的生命周期和引用程序一样长,所以单例中如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的
  • 一些提供close的资源未关闭导致内存泄露

stop the world

stw指的是gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序都会被暂停,没有任何响应,有点像卡死的感觉

可达性分析算法中枚举根节点(gc roots)会导致所以java线程停顿

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系不断变化,则分析结果的准确性无法保证

被stw中断的应用程序会在完成gc后恢复

stw是jvm在后台自动发起和完成的

开发中不要使用System.gc(),会导致stw

并行与并发

并发

  • 在操作系统中,在一个时间段中有几个程序处于已启动到运行完毕之间,且这几个程序都是在同一个处理器上运行
  • 并发不受真正意义上的“同时进行”,只是把一个时间段划分成几个时间片,然后在这个几个时间区间之间来回切换,由于cpu处理速度非常快,只要时间间隔处理得当,就可以让用户感觉是多个应用程序同时在进行
  • 多个事情,在同一个时间段上同时发生
  • 多个任务之间互相抢占资源

并行

  • 当系统有一个以上cpu时,当一个cpu执行一个进程时,另一个cpu可以执行另一个进程,两个进程互不抢占cpu资源,可以同时进行,我们称之为并行
  • 决定并行的因素不是cpu的数量,而是cpu的核心数量
  • 多个事情,在同一个时间点上同时发生
  • 多个任务之间不互相抢占资源

垃圾回收的并行和并发

并行

  • 多条垃圾回收线程并行工作,但此时用户线程仍处于等待状态

串行

  • 相较于并行的概念,单线程执行
  • 如果内存不够,则程序暂停,启动jvm垃圾收集器进行垃圾回收。回收完,再启动程序线程

并发:

  • 用户线程和垃圾收集线程同时执行,垃圾回收线程在执行时不会停顿用户程序的运行
  • 用户程序在继续运行,而垃圾收集在另一个cpu上

安全点和安全区域

安全点

程序在执行时并非在所有时候都能停下来gc,只有在特定的位置才能停顿下来gc,这些位置称为“安全点(safepoint)”

安全点的选择很重要,如果太少可能导致gc等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转和异常跳转

如何在发生gc时,检查所有线程都跑到最近的安全点停顿下来:

  • 抢先式中断(目前没有虚拟机采用):首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点
  • 主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果中断标志位真,则将自己进行中断挂起

安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入gc的安全点。但是,程序“不执行”的时候呢?例如线程处于sleep状态或blocked状态,这时候线程无法响应jvm的中断请求,走到安全点去挂起,jvm也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(safe region)来解决

安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始gc都是安全的。我们也可以把安全区域看作是被扩展了的安全点

实际执行时:

  1. 当线程运行到安全区域的代码时,首先标识已经进入了安全区域,如果这段时间内发生了gc,jvm会忽略标识为安全区域状态的线程
  2. 当线程即将离开安全区域时,会检查jvm是否已经完成gc,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开安全区域的信号为止

引用

在jdk1.2之后,java对引用的概念进行了扩充,将引用分为:

  • 强引用(strong reference):最传统的“引用”定义,是指在程序代码之中普遍存在的引用赋值。物理任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉引用的对象

  • 软引用(soft reference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中

  • 弱引用(weak reference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象

  • 虚引用(phantom reference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

    虚引用必须和引用队列一起使用

者四种引用强度逐渐减弱

除强引用外,其他3种引用均可以在java.lang.ref包中找到,并继承了Reference抽象类

终结器引用:在gc时,终结器引用入队。由finalizer线程通过终结器引用周到被引用对象并调用它的finalize()方法,第二次gc时才能回收被引用的对象

垃圾回收器

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的jvm来实现

由于jdk的版本处于高速迭代中,因此java发展至今已经衍生了众多的gc版本

从不同角度分析垃圾收集器,可以将gc分为不同的类型

经典垃圾收集器

垃圾收集器 分类 作用位置 使用算法 特点 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单cpu环境下的client模式
ParNew 并行 新生代 复制算法 响应速度优先 多cpu环境下的server模式
Paralel 并行 新生代 复制算法 吞吐量优先 后台运算不需要太多交互的场景
Serial Old 串行 老年代 标记-压缩算法 响应速度优先 单cpu环境下的client模式
Parallel Old 并行 老年代 标记-压缩算法 吞吐量优先 后台运算不需要太多交互的场景
CMS 并发 老年代 标记-清除算法 响应速度优先 适用于互联网或b/s业务
G1 并发、并行 新生代、老年代 复制算法、标记压缩算法 响应速度优先 面向服务端应用

查看当前使用的垃圾回收器

  • -XX:+PrintCommandLineFlags:查看命令行相关参数(包括垃圾收集器)
  • 命令行:jinfo -flag 相关垃圾收集器参数 进程id

serial回收器(串行回收)

serial收集器是最基本、历史最悠久的垃圾收集器了。jdk1.3之前回收新生代的唯一选择

serial收集器作为hotspot中client模式下的默认新生代垃圾收集器

serial收集器采用复制算法、串行回收和stw机制的方式进行内存回收

除了年轻代以外,serial收集器还提供了用于执行老年代垃圾收集的serial old收集器。serialold收集器同样采用了串行回收和stw机制,只不过内存回收算法使用了标记-压缩算法

  • serial old是运行在client模式下默认的老年代的垃圾回收期
  • serial old在server模式下主要有两个用途
    • 与新生代的parallel scavenge配合使用
    • 作为老年代cms收集器的后备垃圾收集方案

serial这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个cpu或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(stw)

优点:

  • 简单高效(在单线程下比)
  • 适合client模式

在hotspot中,使用-XX:+UseSerialGC参数指定年轻代和老年代都使用该垃圾收集器

对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在java web应用程序中是不会采用串行垃圾收集器的

ParNew回收器(并行回收)

par是parallel的缩写,new代表只能处理新生代

如果说serial gc是年轻代中的单线程垃圾收集器,那么parNew收集器则是serial收集器的多线程版本

parNew除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间没有任何区别。parNew在年轻代同样采用复制算法、stw机制

parNew是很多jvm运行在server模式下新生代的默认垃圾回收器

在多cpu的环境下,由于可以充分利用多cpu、多核心等物理硬件资源优势,parNew可以更快的完成垃圾收集,提升程序的吞吐量;但在单个cpu的环境下,parNew不比serial效率高

开发人员可以通过-XX:+UseParNewGC手动指定使用ParNew来进行新生代垃圾收集。可以通过-XX:ParallelGCThreads限制线程数量,默认开启和cpu数据相同的线程数

老年代可以使用:

  • cms
  • serial old

ParallelScavenge回收器(吞吐量优先)

hotspot的年轻代中除了拥有parNew收集器是基于并行的以外,ParallelScavemge收集器同样也采用了复制算法、并行回收和stw机制

和parNew收集器不同,ParallelScavenge收集器的目标是达到一个可控的吞吐量,它也被称为吞吐量优先的垃圾收集器

自适应条件策略是ParallelScavenge与ParNew的一个重要区别

高吞吐量可以高效的利用cpu时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此常见在服务器环境中使用

ParallelScavenge在jdk1.6提供了用于执行老年代垃圾收集的ParallelOld收集器,用来替代老年代的serialOld收集器

ParallelOld采用了标记-压缩算法,但同样也是基于并行回收和stw机制

java8中,默认使用ParallelScavenge和ParallelOld的组合

参数配置:

  • -XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务
  • -XX:+UseParallelOldGC:手动指定老年代使用并行回收收集器

以上两个参数,默认开启一个,另一个也会被激活

  • -XX:ParallelGCThreads:年轻代并行收集器的线程数
  • -XX:MaxGCPauseMillis:垃圾收集器最大停顿时间,单位毫秒
  • -XX:GCTimeRatio:垃圾收集时间占总时间的比例
  • -XX:+UseAdaptiveSizePolicy:设置ParallelScavenge收集器具有自适应条件策略。在这种模式下,年轻代的大小,eden和survivor的比例、晋升老年代的年龄等参数都会被自动调整,以达到堆大小、吞吐量和停顿时间之间的平衡点

cms回收器(低延迟)

在jdk1.5时,hotspot退出了一款在强交互应用中有划时代意义的垃圾收集器:cms(concurrent mark sweep)收集器,这款收集器是hotspot中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

cms的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短就越适合用户交互程序,良好的响应速度能提升用户体验

cms的垃圾收集算法采用标记-清除算法,并且也会stw

cms作为老年代收集器,无法与ParallelScavenge配合工作,只能选择ParNew或Serial

cms垃圾收集整个过程分为4步

  1. 初始标记(initial-mark):在这个阶段中,程序中所有的工作线程都会因为stw机制而出现短暂的暂停,这个阶段的主要任务仅仅是标记出gc roots能直接关联到的对象。一旦标记完成后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
  2. 并发标记(concurrent-mark):从gc roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3. 重新标记(remark):由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记纪录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
  4. 并发清除(concurrent-sweep):此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

尽管cms采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段仍需stw。但最耗时的并发标记和并发清除阶段不需要暂停,所以整体回收是低停顿的

由于在垃圾收集阶段用户线程没有中断,所以在回收过程中,还应该保证用户线程有足够的空间可用。因此,cms不能像其他垃圾收集器那样等到老年代满了再进行收集,而是当达到某一阈值时,便开始进行回收。要是预留的内存无法满足程序需要,就会出现Concurrent Mode Filure失败,这时虚拟机将启动后备预案:临时启用serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就长了

cms采用的是标记-清除算法,这意味着每次执行完内存回收后,会不可避免的产生一些内存碎片。那么cms将无法使用指针碰撞,而只能选择空闲列表进行内存分配

因为标记阶段用户线程在继续执行,为了保证用户线程正常执行,不能改变对象的内存地址,所以只能使用标记-清除算法

优点:

  • 并发收集
  • 低延迟

缺点:

  • 会产生内存碎片
  • 对cpu资源非常敏感
  • 无法处理浮动垃圾:在并发标记阶段如果产生新的垃圾对象,cms将无法对这些垃圾对象进行标记

参数:

  • -XX:+UseConcMarkSweepGC:指定老年代使用cms。同时年轻代会使用ParNew
  • -XX:CMSInitialtingOccupanyFration:设置对内存使用率的阈值。jdk5默认为68,jdk6为92
  • -XX:+UseCMSCompactAtFullCollection:用于指定在执行完full gc后对内存空间进行压缩整理(此过程无法并发执行)
  • -XX:CMDFullGCsBeforeCompaction:执行多少次full gc后对内存进行压缩整理
  • -XX:ParallelCMSThreads:设置cms线程数量

在jdk9中cms被标记为废弃,jdk14中cms被彻底删除

g1回收器(区域化分代式)

g1(garbage-first)是在java7时引入的一个新的垃圾回收器

官方给g1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望

为什么叫garbage first(g1)?

  • 因为g1是一个并行回收器,它把堆内存分割为很多不相关的区域(region)(物理上不连续的)。使用不同的region来表示eden、幸存者0区、幸存者1区、老年代等
  • g1有计划的避免在整个java堆中进行全区域的垃圾收集。g1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需的时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region
  • 由于这种方式的侧重点在于回收垃圾最大量的区间(region),所以我们给g1一个名字:垃圾优先(garbage first)

g1是一款面向服务器端应用的垃圾收集器,主要针对配备多核cpu及大容量内存的机器,以极高概率满足gc停顿时间的同时,还兼具了高吞吐量

在jdk9成为默认的垃圾回收器

优势:

  • 并行与并发
    • 并行:在g1回收期间,可用有多个gc线程同时工作,此时用户线程stw
    • 并发:g1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说不会再整个回收阶段发生完全阻塞的情况
  • 分代收集
    • 从分代上看,g1依然属于分代形垃圾回收器。但从堆结构上看,它不要求整个eden区、年轻代或老年代是连续的,也不再坚持固定大小和固定数量
    • 将堆分为若干个region,这些region中包含了逻辑上的年轻代和老年代
    • g1同时兼顾了年轻代和老年代
  • 空间整合
    • g1将内存划分为一个个的region。内存的回收是以region作为基本单位的。region之间是复制算法,但整体上可看作是标记-压缩算法,两种算法都可以避免内存碎片。这种特性有利于程序的长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次gc。尤其是当堆非常大的时候,g1的优势更加明显
  • 可预测的停顿时间模型
    • 由于分区的原因,g1可以只选取部分区域进行回收,这样缩小了范围,可以对全局停顿进行较好的控制
    • g1跟踪各个region垃圾堆积的价值大小,在后台维护一个优先级列表,每次优先回收价值最大的region。保证了g1在有限的时间内可以获取尽可能高的收集效率
    • 相比于cms,g1未必能做到cms最好形况下的停顿时间,但是最差情况下要好很多

缺点:

  • g1需要占用更多的内存
  • 在小内存应用上不如cms

参数:

  • -XX:+UseG1GC:指定使用g1
  • -XX:G1HeapRegionSize:设置每个region的大小。值是2的幂,范围是1mb到32mb之间,目标是根据最小的java堆划分出2048个region。默认是堆内存的1/2000
  • -XX:MaxGCPauseMillis:设置期望达到的最大gc停顿时间指标(不一定能做到),默认200ms
  • -XX:ParallelGCThread:设置stw工作线程的值,最多8
  • -XX:ConcGCThreads:设置并发标记的线程数
  • -XX:InitialtingHeapOccupancyPercent:设置触发并发gc周期的java堆占用率阈值。超过此值,就触发gc。默认45

操作步骤:

  1. 开启g1垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大停顿时间

回收过程:

  1. 年轻代回收(young gc)

    当年轻代的eden区用尽时开始年轻代回收过程:g1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,g1 gc暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到surivor区间或老年代区间,也有可能都涉及

  2. 老年代并发标记(concurrent marking)

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

  3. 混合回收(mixed gc)

    标记完成后马上开始 混合回收过程。对于一个混合回收期,g1从老年代区间移动存活对象到空闲区间,这些空闲区间也就成为老年代的一部分。和年轻代不同,老年代的g1回收器和其他gc不同,g1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的region就可以了。同时,这个老年代region是和年轻代一起被回收的

  4. 如果需要,单线程、独占式、高强度的full gc还是继续存在。它针对gc的评估失败提供了一种失败保护机制,即强力回收

区(region)

使用g1收集器时,它将整个java堆划分为2048个大小相同的独立region块,每个region块大小根据堆空间的实际大小而定,整体被控制在1mb到32mb之间,且为2的n次幂。所以region大小相同,且在jvm生命周期内不会改变

虽然还保留有新生代老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分region的集合。同各国region的动态分配方式实现逻辑上的连续

一个region有可能属于eden、servivor或者old内存区域。但一个region只能属于一个角色(清空之后可以变换角色)

g1垃圾收集器还增加了一种新的内存区域,叫做humongous。主要用于存储大对象,如果超过1.5个region,就放到h中

对于堆中的大对象,默认直接会被分配到老年代,但如果它是一个短期存储的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,g1划分了一个humongous区,它用来专门存放大对象。如果一个h区装不下,那么会用连续的h区来存储。为了找到连续的h区,有时不得不启动full gc。g1的大多数行为都把h区作为老年代的一部分来看待

记忆集(remembered set)

一个region不可能是孤立的,一个region中的对象可能被其他任意region中对象引用,判断对象存活时,释放需要扫描整个堆才能保证准确?其他收集器也有这个问题,而g1更突出

解决方法:

  • 无论g1还是其他分代收集器,jvm都是使用remembered set来避免全局扫描
  • 每一个region都有一个对应的remembered set
  • 每次reference类型数据写操作时,都会产生一个write barrier暂时中断操作
  • 然后检查将要写入的引用指向的对象释放和该reference类型数据在不同的region(其他收集器:检查老年代对象是否引用了新生代对象)
  • 如果不同,通过CardTable把相关引用信息纪录到引用指向对象所在region对应的remembered set中
  • 当进行垃圾收集时,在gc根节点的枚举范围加入remembered set;就可以保证不进行全局扫描,也不会有遗漏

ZGC

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

工作过程:

  1. 并发标记
  2. 并发预备重分配
  3. 并发重分配
  4. 并发重映射

zgc几乎在所有地方都是并发执行的,除了初始标记是stw的。所以停顿时间几乎都耗费在初始标记上

使用-XX:UseZGC开启使用(jdk14之前只支持Linux)

gc日志分析参数

参数 作用
-XX:+PrintGC 输出gc日志。类似:-verbose:gc
-XX:+PrintGCDetails 输出gc的详细日志
-XX:+PrintGCTimeStamps 输出gc的时间戳(标准时间格式)
-XX:+PrintGCDateStamps 输出gc的时间戳(日期格式)
-XX:+PrintHeapAtGC 在进行gc的前后打印出堆信息
-Xloggc:…/logs/gc.log 日志文件的输出路径