# GC
- C 语言编程时,如果调用 malloc() 动态分配了一块内存空间,则需要在用完之后,调用 free() 主动释放内存,否则程序会内存溢出。而 Java 编程时,不需要用户决定如何分配、释放内存。
- 假设用户编写了一个 Java 程序,用 JVM 运行。则 JVM 会自动执行以下操作:
- Java 程序每次新建一个对象,JVM 会自动分配一块内存空间,给新对象使用。
- JVM 会自动找出垃圾对象,删除它们,从而回收其占用的内存空间。该操作称为 GC(Garbage Collection,垃圾回收)。
- 假设用户编写了一个 Java 程序,用 JVM 运行。则 JVM 会自动执行以下操作:
- 参考文档:
# 内存
JVM 运行一个 Java 程序时,至少会创建一个进程,包含多个线程。这些线程分为两类:
- 用户线程:负责执行 Java 程序的代码,或者说用户代码。
- GC 线程:负责 GC 。
JVM 进程占用的内存分为几部分:
- Heap :堆内存。主要存储 Java 类的实例对象。除了堆内存之外的其它内存,统称为非堆内存(noheap)。
- Direct Memory :直接内存,存储与操作系统交互的数据,比如读写文件、Socket 的数据。
- Metaspace :存储 Java 类的元数据,包括常量、注解等。替代了 Java 8 以前的永久代(Permanent)。
- JVM native :运行 JVM 本身占用的内存。
- Code Cache :存储根据 Java 字节码生成的机器代码。
- Thread Stack :线程堆栈。
JVM 进程运行时,会从操作系统申请一些内存空间,划分为 Heap、DirectMemory、Metaspace 等区域,统称为 committed_memory 。
- JVM 申请的内存空间不一定全部使用。committed_memory 中,正在存储数据的那些内存称为 used_memory 。其它内存处于空闲状态,可供未来存储数据,称为 free_memory 。
- 从 JVM 外部、操作系统来看,committed_memory 是整个 JVM 进程占用的内存,会计入主机的 used 内存。
- 从 JVM 内部来看,committed_memory 是一个内存池,其中有的内存正在使用,有的内存空闲。
- 当 JVM 需要存储新数据(比如新建的 Java 对象)时,就会选用一些 free_memory 。如果当前的 free_memory 不足以存入新数据,则可能自动申请更多 committed_memory ,从而凭空增加 free_memory 。也可能自动进行 GC ,从而将 committed_memory 中的某些 used_memory 转换成 free_memory 。
- 通常所说的"内存不足",是指空闲内存不足以存入新数据。即空闲内存的体积,小于新数据的体积。
- 当 JVM 进程申请的 committed_memory 达到 MaxHeapSize、MaxDirectMemorySize 等限制时,就不能继续增加。此时只能依靠 GC 在 Heap 中获得空闲内存。
- GC 一般不会减少 committed_memory ,也不会将 committed_memory 中的空闲内存释放给操作系统,而是留给 JVM 自己未来使用。因为释放内存、重新申请内存的开销较大。
- 如果 GC 之后依然内存不足,则 Java 进程会在终端打印 OutOfMemoryError 报错,表示内存不足。
- java.lang.OutOfMemoryError 属于错误,不属于异常,但可以被 try catch 捕捉。
- 发生 OutOfMemoryError 有什么后果?
- Java 进程可能崩溃退出,也可能继续运行。
- 即使 Java 进程继续运行,也可能因为占用内存过多,被 Linux OOM 机制杀死进程。
- JVM 申请的内存空间不一定全部使用。committed_memory 中,正在存储数据的那些内存称为 used_memory 。其它内存处于空闲状态,可供未来存储数据,称为 free_memory 。
例:假设 Java 进程每隔 1 分钟新建一个对象,则有以下几种结果
- 新对象一直不属于垃圾对象。(比如强引用)
- 此时, GC 算法不能回收内存,因此 committed_memory 会快速增长,直到达到 -Xmx 堆内存容量上限,然后发生 OutOfMemoryError 或 OOM 故障。
- 这种情况称为内存泄漏。
- 有些新对象变为垃圾对象,可以被 GC 算法回收。
- 此时,有些新对象不能被 GC 回收,因此 committed_memory 会慢速增长,属于内存泄漏。可能一段时间之后(比如几天),发生 OutOfMemoryError 或 OOM 故障。
- 上述内存泄漏的问题,是由 Java 程序的源代码决定的,不能通过 GC 算法解决。
- 几乎全部新对象变为垃圾对象,可以被 GC 算法回收。
- 用户编写一个 Java 程序时,应该尽量达到这种效果,从而保证 Java 进程能长时间运行,比如运行几年。
- 此时,依然可能存在一些性能问题:
- GC 时,可能处于长时间的 STW 状态,导致用户线程暂停执行(比如几秒)。用户不一定能忍受这么久的停顿。
- 如果频繁执行 GC 算法,则可能造成较大 CPU 负载,导致用户线程长时间停顿(比如几十秒)。
- 此时的症状:主机 CPU 满载(因为在执行 GC 线程),但 Java 进程假死(因为用户线程停顿),比如 HTTP 端口无响应。
- 遇到上述性能问题时,用户可以尝试优化 GC 算法,有可能解决,或缓解。
- 新对象一直不属于垃圾对象。(比如强引用)
# 引用
GC 时,首先需要找出垃圾对象,如何找出?通常采用以下几种算法:
- 引用计数(Reference Counting)
- 原理:
- 每个对象内置一个引用计数器,表示有多少个地方引用了它。当引用数减为 0 时,就可以删除该对象。
- 创建一个对象并传递给一个变量时,该对象的引用计数为 1 。
- 删除一个对象时,将它引用的其它所有对象的引用技术减 1 。
- 缺点:
- 修改引用计数的开销较大。
- 难以发现循环引用的对象。比如对象 A、B 相互引用,则可能一直不会被删除。
- 原理:
- 跟踪(Tracing)
- 又称为可达性分析。
- 原理:
- 选出当前的局部变量、静态变量、线程等对象,作为根集(GC Roots)。
- 遍历根集中的各个对象,递归寻找它们引用的对象,记录成一条条引用链。
- 不在引用链上的对象,就可以删除。
- 目前跟踪算法更常用。
- 引用计数(Reference Counting)
Java 对象的引用分为四种,从强到弱如下:
- 强引用(Strong Reference)
- :指向一些必须保留的对象,比如
Object obj = new Object()
。 - 即使内存不足,强引用也不会被 GC 算法回收。
- :指向一些必须保留的对象,比如
- 软引用(Soft Reference)
- :指向一些可能有用,但不是必须保留的对象。
- 内存不足时,会被 GC 算法回收。
- 弱引用(Weak Reference)
- :指向一些不需要保留的对象。
- 每次 GC 时,都会被回收。
- 虚引用(Phantom Reference)
- :最弱的引用,相当于没有引用。
- 不能用于获取对象的实例,主要用于判断对象是否被垃圾回收。
- 强引用(Strong Reference)
# STW
- GC 算法一般是先 Mark 找出所有垃圾对象,然后批量删除。而不是每找出一个垃圾对象就删除一个,因为逐个删除的效率低、耗时久。
- 如果在 Mark 过程中,同时运行用户线程、GC 线程,则可能遇到以下问题:
- 错删
- :某些已标记为垃圾的对象,在 Mark 结束之前可能被用户线程重新引用、变为非垃圾对象,然后被删除,导致用户线程执行出错。
- 漏删
- :某些未标记为垃圾的对象,在 Mark 结束之前可能被用户线程改为垃圾对象,没有被本次 GC 删除。
- 这些垃圾对象等到下一次 GC 才会被删除,称为浮动垃圾(floating garbage)。
- 错删
- 为了避免上述问题,JVM 通常在 Mark 过程中处于 STW(Stop The World)状态,此时会暂停执行用户线程,只执行 GC 线程。
- 优点:避免在 Mark 过程中,对象的引用被用户线程改变。
- 缺点:会导致用户线程停顿一定时间,不会工作。
- 不同 GC 算法的 STW 时长不同。与 young GC、old GC 相比,full GC 需要清理全部内存,因此 STW 时间通常最长。
# GC 算法
# Mark-Sweep
- :标记-清除算法。
- 原理:分为两个步骤:
- Mark :找出所有垃圾对象。
- Sweep :删除所有垃圾对象。
- 优点:实现简单,GC 耗时最短。
- 缺点:删除一些垃圾对象之后,释放的空闲内存通常地址不连续,比较零散,容易产生内存碎片,导致内存使用率低。
- 内存碎片是指,某块空闲内存的体积太小,不足以分配给一个对象使用,导致这块空闲内存一直不会被使用。
# Mark-Compact
- :标记-整理算法,全名是 mark-sweep-compact 。
- 原理:在 Mark-Sweep 算法之后,增加一个称为 Compact 的步骤,用于移动所有非垃圾对象的内存地址,从堆内存的头部开始连续存储。
- 优点:不会产生内存碎片。
- 缺点:Compact 步骤增加了 GC 耗时。
# Mark-Copy
:标记-复制算法。
原理:将堆内存分为两个区域 1、2 ,循环使用。步骤如下:
- 最初只使用区域 1 ,不使用区域 2 。
- 等区域 1 内存不足而触发 GC 时,将区域 1 中所有非垃圾对象拷贝到区域 2 ,从区域 2 的头部开始连续存储。然后清空区域 1 的所有数据。
- 接下来只使用区域 2 ,不使用区域 1 。
- 等区域 2 内存不足而触发 GC 时,按上述流程循环使用区域 1 。
优点:
- 与 Mark-Sweep 算法相比,不会产生内存碎片。
- 与 Mark-Compact 算法相比,GC 耗时更少。
缺点:
- 同时只使用一个区域,总内存使用率低于 50% 。
比较 GC 耗时:
- Mark-Sweep 的耗时最短。总耗时主要包括:
- 扫描耗时:与内存容量成正比。
- 删除耗时:与垃圾对象的数量成正比。
- Mark-Copy 的耗时较久,因为复制操作比删除操作慢。总耗时主要包括:
- 扫描耗时
- 复制耗时:与非垃圾对象的数量成正比。
- Mark-Compact 的耗时更久,因为 Compact 操作比 Copy 操作慢。总耗时主要包括:
- 扫描耗时
- 删除耗时
- Compact 耗时:与非垃圾对象的数量成正比。
- Mark-Sweep 的耗时最短。总耗时主要包括:
# Generational Collection
:分代收集算法。
原理:将堆内存分为两个区域,采用不同的 GC 算法:
- 年轻代(young generation):又称为新生代(new generation),用于存储存在时间较短的 Java 对象。预计该区域每分钟都有大量对象变成垃圾,需要经常清理,因此适合采用 Mark-Copy 算法,尽量减少 GC 耗时。
- 老年代(old generation):又称为终身代(tenured generation),用于存储存在时间较长的 Java 对象。预计不需要经常清理,因此适合采用 Mark-Compact 算法。
优点:一般的 Java 程序中,大部分 Java 对象会在创建之后的短期内停止引用,只有少部分对象会长期保留。因此对这两类对象采用不同的 GC 算法,效率更高。
年轻代细分为两种区域:
- 伊甸园空间(eden space):用于存储新创建的对象。
- 幸存者空间(survivor space):用于存储在 young GC 之后幸存、但尚未存入 old 区域的对象。
- 默认创建了两个 survivor space 实例,简称为 S0、S1 ,或称为 From、To 。根据 Mark-Copy 算法循环使用两个实例,比如最初只使用 S0 ,等 GC 时,将 S0 中所有非垃圾对象拷贝到 S1 。
根据作用域的不同,将 GC 分为几种模式:
- young GC
- :又称为 Minor GC 。
- young 区域的 Java 对象存储在 eden space 和一个 survivor space (假设为 S0)中。
- 当 eden space 内存不足时,会触发一次 young GC ,将 eden space 和 S0 中所有非垃圾对象拷贝到另一个 survivor space (这里是 S1 ),然后清空 eden space 和 S0 ,供未来存入新对象。
- 下一次触发 young GC 时,会将 eden space 和 S1 中所有非垃圾对象拷贝到 S0 。
- 如果另一个 survivor space 的空闲内存不足以存入对象,则将对象直接拷贝到 old 区域。如果 old 区域也内存不足,则触发 full GC 。
- 新创建的 Java 对象最初存储在 young 区域的 eden space 。
- 如果对象活过了第一次 young GC ,则从 eden space 拷贝到一个 survivor space 。
- 之后对象每活过一次 young GC ,则从一个 survivor spac 拷贝到到另一个 survivor space 。这样能多过滤几次垃圾对象,减少拷贝到 old 区域的对象数量。
- 如果对象活过 young GC 的次数达到 TenuringThreshold 阈值,则从 survivor space 拷贝到 old 区域,该过程称为长期保留(Tenuring)、晋升(Promotion)。
- JVM 会自动调整 TenuringThreshold 的大小。尽量增加 TenuringThreshold ,从而减少拷贝到 old 区域的对象数量,但不至于让 survivor space 溢出。
- old GC
- :又称为 Major GC 。
- 当 old 区域的内存不足时,会触发一次 old GC ,将 old 区域中存在时间较长的对象删除。
- young GC 、old GC 相互独立,可以同时执行。
- full GC
- :当 old 或 DirectMemory 或 Metaspace 区域的内存不足时,会触发一次 full GC ,对 young、old、DirectMemory、Metaspace 区域全部进行 GC 。
- 目前 full GC 没有严格的标准,有的垃圾收集器的 old GC 相当于 full GC 。
- young GC
young 区域中大部分对象一般会在短期内停止引用,活不到 old 区域,因此两个区域的内存开销不同。
- 例如 HotSpot 认为 old 区域的内存开销一般更大。因此默认配置了
-XX:NewRatio=2
,使得 old 区域容量是 young 的 2 倍。 - 如果 Java 程序短期内创建大量新对象,则可能 young 区域内存不足而频繁 GC ,此时需要增加 young 区域的容量。
- 如果 Java 程序存在大量非垃圾对象,则可能 old 区域内存不足而频繁 GC ,此时需要增加 old 区域的容量。
- 如果 Java 程序存在大量非垃圾对象,还不断创建新的非垃圾对象,则 young、old 区域都会内存不足。
- 例如 HotSpot 认为 old 区域的内存开销一般更大。因此默认配置了
# 垃圾收集器
- 上文列举了几种 GC 算法。但 GC 算法属于理论,实现这些理论的程序称为垃圾收集器(Garbage Collector)。下文列举几个垃圾收集器,一般在 JVM 中内置可用。
# SerialGC
- :串行垃圾收集器。
- 特点:
- 分代收集:young GC 采用 Mark-Copy 算法,old GC 采用 Mark-Compact 算法,全程处于 STW 状态。
- GC 时运行单个 GC 线程。
- 优点:实现简单,是 JVM 最古老的一个垃圾收集器。
- 缺点:堆内存超过 100MB 时,单个 GC 线程的处理速度慢,导致 STW 时间长。
# ParallelGC
- :并行垃圾收集器。
- 特点:
- 分代收集:young GC 采用 Mark-Copy 算法,old GC 采用 Mark-Compact 算法,全程处于 STW 状态。
- GC 时运行多个 GC 线程。
- 优点:堆内存超过 100MB 时,GC 速度比 SerialGC 快几倍。
- 缺点:依然全程处于 STW 状态。
- 类似的几个垃圾收集器:
- ParallelOldGC
- 早期版本的 ParallelGC 只能在 young GC 时创建多个 GC 线程。Java 6 增加了 ParallelOldGC ,在 old GC 时也能创建多个 GC 线程。
- HotSpot 对于 Java 8 ,默认启用
-XX:+UseParallelGC
。并且启用-XX:+UseParallelGC
时,默认会启用-XX:+UseParallelOldGC
。
- ParNewGC
- 专注于 young GC 。在 young GC 时创建多个 GC 线程,在 old GC 时创建单个 GC 线程。
- HotSpot 对于 Java 8 ,启用
-XX:+UseConcMarkSweepGC
时,默认会启用-XX:+UseParNewGC
,从而组合使用两个垃圾收集器,分别处理 young GC、old GC 。
- ParallelOldGC
# ConcMarkSweepGC
:Concurrent Mark Sweep Garbage Collector ,简称为 CMS 垃圾收集器,是第一个支持并发收集的垃圾收集器。
特点:
- 分代收集:young GC 采用 Mark-Copy 算法,处于 STW 状态。old GC 采用 Mark-Sweep 算法并进行了魔改,使得大部分时间不处于 STW 状态。
- young GC、old GC 时支持运行多个 GC 线程。但 full GC 时,只运行单个 GC 线程,处于 STW 状态。
将 old GC 分为多个阶段:
- 初始标记(Initial Mark):最初将所有对象标记为白色,然后找出 GC Roots 直接关联的所有 Java 对象,标记为灰色。
- 并发标记(Concurrent Mark):找出灰色对象引用的所有对象(即使为空),然后将前者标记为黑色,将后者标记为灰色。递归执行该操作,直到不存在灰色对象。最后剩下的白色对象就是不在引用链上的对象,视为垃圾对象。
- 这种标记方案,称为三色标记法。
- 优点:与 Mark-Sweep 算法相比,STW 时长小。
- 缺点:可能漏标。
- 在并发标记阶段,同时运行用户线程、GC 线程。因此可能有些对象刚刚标记,又被用户线程修改了引用。还可能有些对象从 young 区域晋升到 old 区域,加入 old GC 的范围。
- 因此并发标记的结果不可靠,需要后续几个阶段重新标记。
- 即使使用三色标记法,还存在跨带引用的问题:old 区域某些对象,可能引用了 young 区域某些对象,因此与 GC Roots 关联。
- 为了处理跨代引用,young GC 时,需要扫描一遍 young、old 区域的所有可达对象,才能找出 young 区域的垃圾对象。这导致 young GC 的耗时像 full GC 一样久。
- 为了缩短耗时,ConcMarkSweepGC 在 young 区域保存一个特殊的数据结构,称为记忆集(Remember Set,Rset),用于记录所有跨代引用的关系。
- 并发预清洗(Concurrent Preclean):在并发标记期间可能有些对象发生了变化,需要重新标记。
- 并发终止预清洗(Concurrent Abortable Preclean):该阶段像 Concurrent Preclean ,但可通过 JVM 参数限制该阶段的耗时,提前终止。
- 最终重新标记(Final Remark):该阶段进入 STW 状态,检查已标记为垃圾的对象,确保它们依然没有被引用。
- Final Remark 能避免误删,但不能避免漏删,因此可能产生浮动垃圾。
- 并发扫描(Concurrent Sweep):删除垃圾对象。
- 并发重置(Concurrent Reset):重置 ConcMarkSweepGC 垃圾收集器,准备开始下一次 old GC 。
- 上述 5 个并发阶段不处于 STW 状态,虽然耗时长,但不会导致用户线程停顿。
- 其它 2 个阶段虽然处于 STW 状态,但耗时短,可以忍受。
满足以下条件之一时,会触发一次 old GC :
- old 区域的内存使用率,超过 CMSInitiatingOccupancyFraction 阈值。
- Metaspace 内存不足。
- young 区域的对象可能全部晋升到 old 区域,此时如果 old 区域内存不足则会晋升失败,触发 full GC 。为了避免该问题,当 young 区域的 used_memory 大于 old 区域的 free_memory 时,就会触发一次 old GC 。
优点:
- SerialGC、ParallelGC 不支持并发收集,全程处于 STW 状态。而 ConcMarkSweepGC 支持并发收集,使得 old GC 的大部分时间不处于 STW 状态。
缺点:
- young GC 依然处于 STW 状态。
- old GC 会产生浮动垃圾,导致 old 区域占用内存增加 10% 左右。
- old GC 采用 Mark-Sweep 算法,容易产生内存碎片。
- 不采用 Mark-Compact、Mark-Copy 算法,是因为改变对象的内存地址时,必须处于 STW 状态。
- JVM 对 ConcMarkSweepGC 默认启用了
-XX:+UseCMSCompactAtFullCollection
功能,在 full GC 时自动压缩内存碎片。
HotSpot 从 Java 9 开始,默认采用 G1GC ,并且弃用 ConcMarkSweepGC 。
- 如果用户启用 ConcMarkSweepGC ,则 JVM 会打印 warning 警告。
# G1GC
:Garbage First Garbage Collector ,垃圾优先的垃圾收集器。
上述几个传统的垃圾收集器,将堆内存分为 young、old 两个区域,以 bytes 为单位管理内存。而 G1GC 以 region 为单位管理管理。
- 在物理上,G1GC 将堆内存分为很多个 region 空间。
- 每个 region 是一小块地址连续的内存空间,用于存储数据。
- 每个 region 的容量相同,默认为几 MB 。
- 如果一个对象的体积,超过 region 容量的 50% ,则称为 humongous object 。一个 region 可能容不下该对象,会用几个地址连续的 region 存储该对象。
- 在逻辑上,G1GC 将堆内存分为 eden、survivor、old 三个区域,并记录每个区域分别由哪些 region 组成。
- 改变某个区域包含的 region 集合,就可以增减该区域的容量。
- 在物理上,G1GC 将堆内存分为很多个 region 空间。
G1GC 定义了三种 GC 模式:
- young GC
- :清理年轻代中的 eden 区域,然后将其中幸存的对象,拷贝到 survivor 区域。
- 拷贝的过程称为撤离(evacuation)。
- mixed GC
- :清理年轻代的所有 region ,还会清理老年代包含较多垃圾对象的 region 。
- full GC
- G1GC 在 full GC 时,不会并发收集。而是单线程收集,采用 mark-sweep-compact 算法,容易导致长时间的 STW 。
- 因此,应该尽量让 G1GC 只进行 young GC 和 mixed GC ,避免 full GC 。
- young GC
mixed GC 分为多个阶段:
- 初始标记(initial mark)
- 此时发生一段短时间的 STW 。
- 该阶段的目标:标记 GC Roots 直接关联的所有 Java 对象。
- 当堆内存使用率达到 -XX:InitiatingHeapOccupancyPercent 百分比时,G1GC 就会触发一次 mixed GC 。但不会立即开始 mixed GC ,而是等待下一次 young GC ,在 young GC 的 STW 期间,趁机进行 mixed GC 的初始标记。
- 根区域扫描(root region scan)
- 此时不会 STW 。
- 该阶段的目标:标记 survivor 区域引用的所有老年代 Java 对象。
- 并发标记(concurrent mark)
- 此时不会 STW 。
- 该阶段的目标:扫描所有老年代 region ,如果某个 region 包含较多垃圾对象,则记录到 CSet 中,准备待会清理这个 region 。
- 该策略称为垃圾优先。即只清理包含较多垃圾对象的 region ,这样容易回收内存。
- 如果清理所有 region ,虽然能回收更多内存,但 GC 耗时更久。
- 每次 mixed GC 时,G1GC 会创建一个 CSet(Collection Set) 数据结构,用于记录哪些老年代 region 等待清理。
- 为了更快地扫描 region ,G1GC 会在 Java 进程启动时,为每个 region 创建一个 Rset 数据结构,用于记录哪些 region 引用了当前 region 中的对象。
- 这样 GC 时只需扫描 RSet ,不需要扫描 region 中的所有对象。
- 即使这样,并发标记依然耗时较久,期间可能发生多次 young GC 。
- 重新标记(remark)
- 此时发生一段短时间的 STW 。
- 该阶段的目标:并发标记时采用三色标记法,可能漏标,因此需要进入 STW 状态,重新标记。
- G1GC 采用 SATB(Snapshot At The Beginning)算法来重新标记,比 ConcMarkSweepGC 的重新标记更快。
- 清理(cleanup)
- 此时发生一段短时间的 STW 。
- 该阶段的目标:将 Cset region 中的非垃圾对象,拷贝、集中到一些空闲 region 中,然后 Cset region 清空,变为空闲 region 。
- 初始标记(initial mark)
优点:
- 只要不发生 full GC ,STW 时长就较短。
- 可以通过 -XX:MaxGCPauseMillis 命令行参数,软性限制 STW 时长,这样容易预测 STW 时长,避免停顿太久。
- 处理大量内存的性能更好。
- 以 region 为单位管理内存空间,容易增减 eden、survivor、old 三个区域的容量。
- ConcMarkSweepGC 容易产生内存碎片,而 G1GC 通过拷贝、集中内存中的数据,避免了内存碎片。
- 与 G1GC 相比,传统的 SerialGC、ParallelGC 只适合处理 4G 以下的堆内存,因为堆内存越大,STW 时长越久。
缺点:
- 需要创建大量 Rset 数据结构,占用堆内存的 5% 左右。
# 性能指标
- 在不同的使用场景下,可能选用不同的垃圾收集器。为了比较它们的优劣,通常考虑以下性能指标:
- Footprint
- :堆内存使用量。
- Latency
- :GC 时 STW 会导致用户线程停顿。
- 例如原来 Java 程序执行一段代码的耗时为 1s ,遇到 STW 则增加到 1.1s 。
- 吞吐量(Throughput)
- :用户线程占用的 CPU 时间的比例。
- 例:假设 JVM 总共占用了 1s 时长的 CPU ,其中用户线程占用了 0.9s ,GC 线程占用了 0.1s ,则吞吐量为 90% 。
- 吞吐量越高,用户线程占用的 CPU 时间越多,因此能执行更多业务代码。
- ConcMarkSweepGC、G1GC 通过并发收集减少了 STW 时长,但增加了总的 GC 耗时。它们同时运行用户线程、GC 线程,因此降低了用户线程的吞吐量。
- Footprint
- 上述三个指标最多同时追求两个。例如追求低延迟、高吞吐量,则减少了 GC 线程的执行时长,因此 GC 效果差,会占用更多堆内存。
- 用户可根据自己的需求,优化 JVM 配置参数,从而提升性能。例如:
- 如果 Java 程序直接为用户提供服务,追求低延迟,则采用 G1GC 。
- 如果 Java 程序不追求低延迟,而是追求高吞吐量、低成本,则采用 ParallelGC 。
- JVM 优化的幅度有限,可能只提升 30% 的性能,而且需要用户学习如何优化 JVM 并调试验证。
- 如果 Java 程序存在大量非垃圾对象,超出了堆内存容量,则 JVM 优化也无济于事。只能增加内存,或者优化 Java 代码,例如通过分页查询减少 Java 程序同时处理的数据量、将来不及处理的数据暂存到 Kafka 。
- 用户可根据自己的需求,优化 JVM 配置参数,从而提升性能。例如: