June's Studio.

Java-垃圾回收

字数统计: 6.5k阅读时长: 24 min
2023/03/26

判断对象是否可回收

如何判断一个对象属于垃圾对象呢?

引用计数法

对于一个对象 A,只要有任意一个对象引用了 A,则 A 的计数器加 1,当引用失效的时候,引用计数器就减 1。如果 A 的应用计数器为 0,则对象 A 就不可能再被使用。

缺点:虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。

可达性分析算法

通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的循环称为引用链。当一个对象到 GC Roots 没有任何引用链的时候,则证明此对象是不可达的,因此它们会被判定为可回收对象

可以作为 GC Roots 的对象:

  • 类静态属性中引用的对象
  • 常量引用的对象
  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象

finalize 方法中复活

finalize() 方法只会被调用一次:

1
2
3
4
5
@Override
protected void finalize() throws Throwable {
super.finalize();
obj = this;
}

下述代码在内存中如何放置的示例:

1
StringBuffer str = new StringBuffer("Hello world");

假设以上代码是在函数体内运行的,那么:

四个引用

  • 软引用: java.lang.ref.SoftReference 可被回收的引用
  • 弱引用: 发现即回收。由于垃圾回收器的线程通常优先级很大,因此并不一定很快地发现持有弱引用的对象。
  • 虚引用: 跟踪垃圾回收过程

内存泄露

1
2
3
4
5
6
7
while (true) {
for (int i=0; i<10000; i++) {
if (!m.contains(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}

垃圾回收算法

标记 - 清除算法

从每个 GC Roots 对象出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。

缺点:带来大量空间碎片,导致需要分配一个较大连续空间时,容易触发 GC

碎片化:

标记 - 整理 (标记 - 压缩) 算法

从每个 GC Roots 对象出发,标记存活的对象,然后将存活的对象整理到内存空间的一端,形成连续的已使用空间,最后将已使用空间外的部分全部清理掉,消除空间碎片问题。

标记 - 复制算法

为了能够并行的标记和整理,将整个空间分为两块,每次只激活一块,垃圾回收只需把存活的对象复制到另一块未激活的空间上,将未激活空间标记为已激活,将已激活空间标记为未激活,然后清除原空间中的原对象。

分代收集算法

垃圾收集器一般根据对象存活周期的不同,将内存划分为几块,根据每块内存空间的特点,使用不同的回收算法,提供回收效率。

分区算法

将整个堆空间划分为连续的不同小空间,每一个小空间独立使用,独立回收。

优点:可以控制一次回收多少个小区间。

HotSpot 虚拟机垃圾收集器

Serial

新生代 Serial 收集器采用复制算法,使用单线程进行垃圾回收,回收时 Java 应用程序中的线程都需要暂停 (Stop-The-World),以等待回收完成。使用 -XX:+UseSerialGC 可以指定新生代采用 Serial 收集器,老年代采用 Serial Old 收集器。虚拟机在 Client 模式下运行时,它是默认的垃圾收集器。独占式回收

它的日志格式如下:

ParNew

新生代 ParNew 将 Serial 收集器多线程化,在并发能力强的 CPU 上,产生的停顿时间短于串行回收器。开启 ParNew 回收器:

  • -XX:+UseParNewGC:新生代 ParNew,老年代采用 Serial Old
  • -XX:+UseConcMarkSweepGC:新生代 ParNew,老年代采用 CMS

-XX:ParallelGCThreads 可以指定并行回收的线程数,这个线程数的默认值是:

1
2
3
4
5
6
7
threadsNum = 0;

if (CORE_OF_CPU < 8) {
threadsNum = CORE_OF_CPU;
} else {
threadsNum = 3 + 5 * CORE_OF_CPU / 8;
}

它的 GC 日志格式如下:

Parallel

新生代 Parallel 采用复制算法,多线程、独占式,它与 ParNew 的不同之处:

  • 关注系统的吞吐量
  • 支持自适应 GC 调节

以下参数启用 Parallel 回收器:

  • -XX:+UseParallelGC:新生代 ParallelGC,老年代:Serial Old
  • -XX:+UseParallelOldGC:新生代 ParallelGC,老年代 ParallelOldGC

用于控制吞吐量的两个重要参数:

  • -XX:MaxGCPauseMills: 设置最大垃圾收集停顿时间。
  • -XX:GCTimeRatio: 设置吞吐量大小,范围 0 ~ 100。假设这个值是 n,那么默认不超过 1 / (1 + n) 的时间百分比用于垃圾收集,n 默认为 99

用于控制自适应调节 GC 的参数:

  • -XX:+UseAdaptiveSizePolicy: 新生代、eden 和 survivor 的比例会动态调整。

它的 GC 日志格式如下:

Serial Old

老年代串行收集器 Serial Old 采用 ** 标记 - 整理 (标记 - 压缩)** 算法,也使用单线程进行垃圾回收。使用如下参数开启 Serial Old 回收器:

  • -XX:+UseSerialGC:新生代、老年代都使用 Serial 回收器 (老年代用的是 Serial Old)
  • -XX:+UseParNewGC:新生代采用 ParNew,老年代采用 Serial Old
  • -XX:+UseParallelGC:新生代采用 ParallelGC,老年代采用 Serial Old

它的日志格式如下:

Parallel Old

老年代 Parallel Old 回收器采用标记 - 整理算法,多线程进行垃圾回收。使用 -XX:+UseParallelOldGC 可以在新生代采用 Parallel,老年代采用 Parallel Old 收集器。参数 -XX:ParallelGCThreads 可以用于设置垃圾回收时的线程数量。

它的 GC 日志格式如下:

CMS

CMS JDK9 被标记弃用,JDK14 被删除。

CMS 是一个基于标记 - 清除的算法,启用 CMS 的参数是 -XX:+UseConcMarkSweepGC,默认启动的工作线程数是 (ParallelGCThreads + 3) / 4。 CMS 不会等到堆内存饱和的时候才进行垃圾回收,而是当老年代的堆内存使用率达到某个阈值 -XX:CMSInitiatingOccupancyFraction (默认是 68%) 的时候便开始进行回收。CMS 基于标记 - 清除算法,因此执行垃圾回收完毕之后,会出现大量内存碎片,造成如果需要将内存分配给较大的对象,则必须被迫进行一次垃圾回收,以换取连续的内存空间。未解决这个问题,可以使用 -XX:+UseCMSCompactAtFullCollection 开关,使得 CMS 垃圾收集完毕之后,进行一次内存碎片整理;-XX:CMSFullGCsBeforeCompaction 参数可以用于设定进行多少次 CMS 回收后,执行一次内存压缩。

它的 GC 日志格式如下:

CMS 的代价:应用程序消耗更多的 CPU。

G1

G1 是专门针对以下应用场景设计的:

  • 像 CMS 收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要 GC 停顿时间更好预测。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的 Java Heap。

使用 -XX:+UseG1GC 可以打开 G1 收集器开关。参数 -XX:MaxGCPauseMills 可以调整最大停顿时间,另外一个参数 -XX:ParallelGCThreads 可以设置并行回收时,GC 的工作线程数量。

G1 引入的目的?

是为了缩短处理超大堆的停顿时间

G1 相比 CMS?

  • G1 是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
  • G1 的 Stop The World(STW) 更可控,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

G1 和 CMS 执行的操作其实是一样的:

  • 并发全局扫描标记检查存活的对象
  • 哪些区域垃圾对象最多,G1 就先收集哪些区域,这也是它为什么称为 Garbage-First 的原因

G1 如何做到可预测的暂停时间?

G1 回收的第 4 步,它是 “选择一些内存块”,而不是整代内存来回收,这是 G1 跟其它 GC 非常不同的一点,其它 GC 每次回收都会回收整个 Generation 的内存 (Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而 G1 每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。

G1 坏处

应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个 Heap 的回收,那么 G1 要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。

G1 的 Region

G1 的各代存储地址是不连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个 Region 占有一块连续的虚拟内存地址。如下图所示:

在上图中,我们注意到还有一些 Region 标明了H,它代表 Humongous,这表示这些 Region 存储的是巨大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。

一个 Region 的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从 1M32M,且是 2 的指数。如果不设定,那么 G1 会根据 Heap 大小自动决定。

G1 采用的算法

从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:

  • 新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World 的暂停。
  • 老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。

ZGC

JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。

默认垃圾收集器

参考 1 说: 对于 server-class 机器,默认 GC 从 serial 改为了 parallel 收集器。

参考 2 says: 从 Java 5.0 开始就会检测运行在 server-vm 还是 client-vm。对于 Java SE 6, 拥有 2 个 CPU、2GB 物理内存的机器属于 server-class 的机器。

Java 7 和 Java 8 使用的都是 Parallel GC,Java 9 使用的是 G1 垃圾收集器。

垃圾回收器怎么选择

  • 最小化地使用内存和并行开销,请选择 Serial GC
  • 最大化应用程序的吞吐量,请选择Parallel GC
  • 最小化 GC 的中断或者停顿时间,请选择 CMS GC

并发和并行都可以表示两个或者多个任务一起执行,但是偏重点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的 “同时执行”。

Full GC

什么情况下会触发 FullGC ? 参考

Full GC vs MajorGC

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

  • Partial GC:并不收集整个 GC 堆的模式
    • Young GC:只收集 young gen 的 GC
    • Old GC:只收集 old gen 的 GC。只有 CMS 的 concurrent collection 是这个模式
    • Mixed GC:收集整个 young gen 以及部分 old gen 的 GC。只有 G1 有这个模式
  • Full GC:收集整个堆,包括 young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

Major GC 通常是跟 full GC 是等价的,收集整个 GC 堆。但因为 HotSpot VM 发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说 “major GC” 的时候一定要问清楚他想要指的是上面的 full GC 还是 old GC。

GC 触发策略

最简单的分代式 GC 策略,按 HotSpot VM 的 serial GC 的实现来看,触发条件是:

  • young GC:当 young gen 中的 eden 区分配满的时候触发。注意 young GC 中有部分存活对象会晋升到 old gen,所以 young GC 后 old gen 的占用量通常会有所升高。
  • full GC:当准备要触发一次 young GC 时,如果发现统计数据说之前 young GC 的平均晋升大小比目前 old gen 剩余的空间大,则不会触发 young GC 而是转为触发 full GC(因为 HotSpot VM 的 GC 里,除了 CMS 的 concurrent collection 之外,其它能收集 old gen 的 GC 都会同时收集整个 GC 堆,包括 young gen,所以不需要事先触发一次单独的 young GC);或者,如果有 perm gen 的话,要在 perm gen 分配空间但已经没有足够空间时,也要触发一次 full GC;或者 System.gc()、heap dump 带 GC,默认也是触发 full GC。

HotSpot VM 里其它非并发 GC 的触发条件复杂一些,不过大致的原理与上面说的其实一样。当然也总有例外。Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发 full GC 前先执行一次 young GC,并且两次 GC 之间能让应用程序稍微运行一小下,以期降低 full GC 的暂停时间(因为 young GC 会尽量清理了 young gen 的死对象,减少了 full GC 的工作量)。控制这个行为的 VM 参数是 - XX:+ScavengeBeforeFullGC。并发 GC 的触发条件就不太一样。以 CMS GC 为例,它主要是定时去检查 old gen 的使用量,当使用量超过了触发比例就会启动一次 CMS GC,对 old gen 做并发收集。

System.gc()

默认情况下 (即未开启 -XX:+DisableExplictGC 参数的情况下),调用 System.gc() 会显示触发 FullGC,同时对新生代和老年代进行回收。

老年代空间不足

当老年代空间新生代对象转入创建大对象大数组时,空间不足,会触发 FullGC,如果触发完依然不足,则抛出如下错误:

1
java.lang.OutOfMemoryError: Java heap space

永生区空间不足

Permanet Generation 中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。

CMS 晋升失败

CMS GC 时出现 promotion failed 和 concurrent mode failure

promotion failed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure 是在

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候 “空间不足” 是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC)

堆中分配很大的对象

所谓大对象,是指需要大量连续内存空间的 java 对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发 JVM 进行 Full GC。

jmap -histo:live

执行 jmap -histo:live [强制 Full GC,不加 live 就不会] 或者 jmap -dump:live [强制 full gc,不加 live 就不会]

对象何时进入老年代

老年对象达到年龄

新生代的对象,每经历一次 GC,年龄加 1,当年龄的最大值最多达到 MaxTenuringThreshold (默认值 15) 的情况下,就可以晋升到老年代。

对象的实际晋升年龄是根据 survivor 区的使用情况动态计算的。

大对象

新生代空间无法容纳大对象,则会直接晋升到老年代。

参数 PretenureSizeThreshold 可以设置对象直接晋升到老年代的阈值,单位是字节,不过只对 SerialParNew 收集器有效,默认值为 0,即不指定最大晋升大小。

GC 有无问题

来自美团技术公众号 2019-2021 后端合集

评价指标

  • 延迟(Latency):也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。
  • 吞吐量(Throughput):应用系统的生命周期内,由于 GC 线程会占用 Mutator 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比,例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。

目前各大互联网公司的系统基本都更追求低延时,避免一次 GC 停顿的时间过长对用户体验造成损失,简而言之,即为**一次停顿的时间不超过应用服务的 TP9999,GC 的吞吐量不小于 99.99%**。举个例子,假设某个服务 A 的 TP9999 为 80 ms,平均 GC 停顿为 30 ms,那么该服务的最大停顿时间最好不要超过 80 ms,GC 频次控制在 5 min 以上一次。如果满足不了,那就需要调优或者通过更多资源来进行并联冗余。

分析工具

命令行终端

  • 标准终端类:jps、jinfo、jstat、jstack、jmap
  • 功能整合类:jcmd、vjtools、arthas、greys

可视化界面

  • 简易:JConsole、JVisualvm、HA、GCHisto、GCViewer
  • 进阶:MAT、JProfiler

读懂 GC Cause

拿到 GC 日志,我们就可以简单分析 GC 情况了,通过一些工具,我们可以比较直观地看到 Cause 的分布情况,如 gceasy 等。

垃圾调优策略

面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,需要提高对象的回收效率;在 CPU 使用率高的情况下,需要降低高并发时垃圾回收的频率。可以说,垃圾回收的调优是一项必备技能。

Mutator 类型

  • IO 交互型:互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡,Young 区越大越好。
  • MEM 计算型:主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。

对象 Survival Time 分布图,对我们设置 GC 参数有着非常重要的指导意义,如下图就可以简单推算分代的边界。

启动时 GC 次数较多

GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整。

原因:在 JVM 的参数中 -Xms-Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。另外,如果空间剩余很多时也会进行缩容操作,JVM 通过 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。

解决:尽量将成对出现的空间大小配置参数设置成固定的,保证 Java 虚拟机的堆是稳定的,如 -Xms-Xmx-XX:-MaxNewSize-XX:NewSize-XX:MetaSpaceSize-XX:MaxMetaSpaceSize 等。

System.gc

增加 -XX:+DisableExplicitGC 此方法变成一个空方法:

1
2
3
4
5
6
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
JVMWrapper("JVM_GC");
if (!DisableExplicitGC) {
Universe::heap()->collect(GCCause::_java_lang_system_gc);
}
JVM_END

如果没有加的话便会引发一次 STW 的 Full GC :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


void GenCollectedHeap::collect(GCCause::Cause cause) {
if (should_do_concurrent_full_gc(cause)) {
#if INCLUDE_ALL_GCS

collect_mostly_concurrent(cause);
#else
ShouldNotReachHere();
#endif
} else {
#ifdef ASSERT
if (cause == GCCause::_scavenge_alot) {

collect(cause, 0);
} else {

collect(cause, n_gens() - 1);
}
#else

collect(cause, n_gens() - 1);
#endif
}
}

堆内存由 JVM 自己管理,堆外内存必须要手动释放,DirectByteBuffer 没有 Finalizer,它的 Native Memory 的清理工作是通过 sun.misc.Cleaner 自动完成的,是一种基于 PhantomReference 的清理工具,比普通的 Finalizer 轻量些。

DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory,下面是代码实现:

1
2
3
4
5
6
7
8
9
10
DirectByteBuffer(int cap) {

Bits.reserveMemory(size, cap);
}

static void reserveMemory(long size, int cap) {


System.gc();
}

Old GC 对 Old 对象做 Reference Processing,Young GC 对 Young 对象做 Reference Processing,做这个 Reference Processing 的时候可以清理掉 DirectByteBuffer。但是如果很长一段时间里没做过 GC 或者只做了 Young GC 的话则不会在 Old 触发 Cleaner 的工作,那么就可能让本来已经死亡,但已经晋升到 Old 的 DirectByteBuffer 关联的 Native Memory 得不到及时释放。如果打开了 -XX:+DisableExplicitGC,清理工作就可能得不到及时完成,于是就有发生 Direct Memory 的 OOM。

降低 Minor GC 频率

通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间 (-Xmn) 来降低 Minor GC 的频率。

可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效果呀。

我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为 :T1+T2

当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1

可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本

如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。

Minor GC 时间太长

减小新生代空间大小

降低 Full GC 的频率

  • 减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。

  • 增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。

脉脉 : 频繁 Full GC

(1)第一种

平时处理经验:

  1. 看下 JVM 参数的设置,是否有明显不合理的,用的什么类型的 GC,JDK 版本
  2. 看一下 gclog (JVM 带了参数 -xloggc 就会有),确定 Full GC 原因,比如 JDK 1.8 的 Metaspace 区域默认只有 20.8 MB,项目如果加载类特别多,那么也会不断 Full GC 扩容。通过 gclog,可以发现很多信息,可以知道 Full GC 前后各个区域的空间占用情况如何。
  3. 如果是内存无法释放,就 DUMP 内存,通过 MAT 去分析是哪一块代码出现了问题。

(2)第二种

  1. 下线机器
  2. jmap 看下实例,jstack 保留堆栈,有的问题到这里应该就可以解决了
  3. dump,重启 Tomcat

(3)第三种

  1. 建议 case by case 分析
  2. 你说 dump 分析,之前遇到的是 xx 场景,推测是大对象引起的,如何优化
  3. 不要一下子所有情况都说了

(4)第四种

  1. 运维肯定有 CPU 告警,大点的公司都有 Full GC 告警,没有上去就直接 jstat -gcutil 的。

老年代 GC 时间过长

通常使用 parallelGC 或者 parallelOldGC 的话,增加老年代空间无法显著降低 GC 时间,可以改用 CMS

选择合适的 GC 回收器

假设我们有这样一个需求,要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,CMS(Concurrent Mark Sweep)回收器和 G1 回收器都是不错的选择。

而当我们的需求对系统吞吐量有要求时,就可以选择 Parallel Scavenge 回收器来提高系统的吞吐量。

JVM 命令

查看上次 GC 原因

查看当前对象数量

Dump Java 堆

第三方

参考

CATALOG
  1. 1. 判断对象是否可回收
    1. 1.1. 引用计数法
    2. 1.2. 可达性分析算法
    3. 1.3. finalize 方法中复活
    4. 1.4. 四个引用
    5. 1.5. 内存泄露
  2. 2. 垃圾回收算法
    1. 2.1. 标记 - 清除算法
    2. 2.2. 标记 - 整理 (标记 - 压缩) 算法
    3. 2.3. 标记 - 复制算法
    4. 2.4. 分代收集算法
    5. 2.5. 分区算法
  3. 3. HotSpot 虚拟机垃圾收集器
    1. 3.1. Serial
    2. 3.2. ParNew
    3. 3.3. Parallel
    4. 3.4. Serial Old
    5. 3.5. Parallel Old
    6. 3.6. CMS
    7. 3.7. G1
    8. 3.8. ZGC
    9. 3.9. 默认垃圾收集器
    10. 3.10. 垃圾回收器怎么选择
  4. 4. Full GC
    1. 4.1. Full GC vs MajorGC
    2. 4.2. GC 触发策略
    3. 4.3. System.gc()
    4. 4.4. 老年代空间不足
    5. 4.5. 永生区空间不足
    6. 4.6. CMS 晋升失败
    7. 4.7. 堆中分配很大的对象
    8. 4.8. jmap -histo:live
  5. 5. 对象何时进入老年代
    1. 5.1. 老年对象达到年龄
    2. 5.2. 大对象
  6. 6. GC 有无问题
    1. 6.1. 评价指标
    2. 6.2. 分析工具
    3. 6.3. 读懂 GC Cause
  7. 7. 垃圾调优策略
    1. 7.1. Mutator 类型
    2. 7.2. 启动时 GC 次数较多
    3. 7.3. System.gc
    4. 7.4. 降低 Minor GC 频率
    5. 7.5. Minor GC 时间太长
    6. 7.6. 降低 Full GC 的频率
    7. 7.7. 脉脉 : 频繁 Full GC
    8. 7.8. 老年代 GC 时间过长
    9. 7.9. 选择合适的 GC 回收器
  8. 8. JVM 命令
    1. 8.1. 查看上次 GC 原因
    2. 8.2. 查看当前对象数量
    3. 8.3. Dump Java 堆
    4. 8.4. 第三方
  9. 9. 参考