最近看了一篇博文《 代码触发JVM的Full GC和Young GC》,代码触发GC,上手试了一下,分析理解GC,同时再重新整理一下GC的东西,实践一下费曼学习法。
GC 概念
什么是 GC
GC garbage collection,垃圾回收,这里特指 JVM 内存回收。
垃圾回收出现之前,编写代码时需要自行申请和管理内存,容易出现各种内存泄漏,导致程序崩溃。为了解决这个痛点,降低编程门槛,带垃圾回收的编程语言就出现了。它能对不使用的内存进行回收,帮我们实现自动内存管理,减少开发工作。
GC 的基本思路是对使用到的对象进行标记,剩余没标记到的对象就是可清除的,对其进行回收再利用,进而达到持久的内存管理。
标记算法现行的主流是根搜索算法,通过 JVM 中定义的根对象(GC root)开始,从这些不可回收的对象开始,进行有向图遍历把使用的对象进行标记,那么剩余的未标记就是我们回收清除的对象。
常见的根对象包括:
栈中引用的对象 (在用呢)
类的静态属性引用的对象
常量引用的对象
Native方法引用的对象
…
Java GC 算法
垃圾回收的基础思路是标记清除,基于标记清除算法又优化出了标记整理算法、复制算法以及分代算法。
算法 | 实现 | 特点 | 场景 |
---|---|---|---|
标记整理 | 标记清除+移动对象整理碎片 | 需要遍历多次,时间效率低,空间利用率高 | 回收变动较小,空间需求大时 |
复制 | 分成From和To,遍历时复制可用对象至To,遍历完清除From,来回置换 | 遍历1次,时间效率高,空间利用低 | 回收变动频繁 |
分代 | 标记整理 + 复制,新对象用复制,老对象用标记整理 | 杂糅 | 新生代处理新对象,一般寿命较短;年老代处理旧对象,一般变动较少 |
Java GC 分类
Java GC 主要是对 JVM 进程的内存管理,尤其是堆(Heap)内存和元空间(Meta Space)的管理。分代算法中存在两种不同的类型,所以主要有YoungGC(也叫Minor GC), FullGC两个。
堆 = 新生代 + 年老代; 默认 1:2
新生代 = Eden + Survivor 0 + Survivor 1(也叫 From, To);默认是 8:1:1
YoungGC 是回收 YoungGen 区域 Eden + S0(From),S1 (To) 区为置空区;
FullGC 是主要内存区域的回收,所以有 YGC(新生代),OldGC(年老代),元空间回收(metaspace)。
简单代码演示
code
这里开篇提到的博文,如何使用代码实现运行时的 5次ygc,然后3次fgc,然后3次ygc,然后1次fgc,请给出代码以及启动参数。
先看代码
public class GCTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
System.out.println("0.---");
List caches = new ArrayList();
for(int i=0; i<11; i++){
caches.add(new byte[3*_1MB]);
}
System.out.println("1.----");
caches.add(new byte[3* _1MB]);
caches.remove(0);
caches.add(new byte[3*_1MB]);
for(int i=0;i<8;i++){
caches.remove(0);
}
caches.add(new byte[3* _1MB]);
System.out.println("2.----");
for(int i=0;i<7;i++){
caches.add(new byte[3* _1MB]);
}
}
}
代码很简单,核心思路就是限制新生代、年老代的大小,通过生成适当大的对象,触发YGC,移动大对象到年老代;之后通过释放部分对象,触发FGC;再释放再触发达到题目的要求。
配置参数
JVM的配置参数时
-Xms41m -Xmx41m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-X 配置 内存
-XX 配置参数 + 使用该参数 -忽略该参数
可以看一下GC日志(简略版)
0.080: [GC (Allocation Failure) [PSYoungGen: 7127K->736K(9216K)] 7127K->6888K(41984K), 0.0018287 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.082: [GC (Allocation Failure) [PSYoungGen: 7040K->704K(9216K)] 13192K->13000K(41984K), 0.0019527 secs] [Times: user=0.08 sys=0.02, real=0.00 secs]
0.085: [GC (Allocation Failure) [PSYoungGen: 6980K->688K(9216K)] 19276K->19128K(41984K), 0.0017549 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.087: [GC (Allocation Failure) [PSYoungGen: 6974K->656K(9216K)] 25414K->25240K(41984K), 0.0017696 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.089: [GC (Allocation Failure) [PSYoungGen: 6948K->704K(9216K)] 31532K->31432K(41984K), 0.0017646 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.091: [Full GC (Ergonomics) [PSYoungGen: 704K->0K(9216K)] [ParOldGen: 30728K->31249K(32768K)] 31432K->31249K(41984K), [Metaspace: 2655K->2655K(1056768K)], 0.0068850 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
0.099: [Full GC (Ergonomics) [PSYoungGen: 6296K->3072K(9216K)] [ParOldGen: 31249K->31249K(32768K)] 37545K->34321K(41984K), [Metaspace: 2655K->2655K(1056768K)], 0.0055871 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
0.105: [Full GC (Ergonomics) [PSYoungGen: 6144K->0K(9216K)] [ParOldGen: 31249K->12817K(32768K)] 37393K->12817K(41984K), [Metaspace: 2655K->2655K(1056768K)], 0.0034627 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.109: [GC (Allocation Failure) [PSYoungGen: 6299K->32K(9216K)] 19116K->18993K(41984K), 0.0008367 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.110: [GC (Allocation Failure) [PSYoungGen: 6332K->32K(9216K)] 25293K->25137K(41984K), 0.0007602 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.111: [GC (Allocation Failure) [PSYoungGen: 6305K->32K(9216K)] 31411K->31281K(41984K), 0.0007003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.112: [Full GC (Ergonomics) [PSYoungGen: 32K->0K(9216K)] [ParOldGen: 31249K->31249K(32768K)] 31281K->31249K(41984K), [Metaspace: 2655K->2655K(1056768K)], 0.0013869 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
可增加了 -XX:+PrintHeapAtGC 能够在GC触发前,打印堆得使用信息,方便理解。这里只显示一个
{Heap before GC invocations=1 (full 0):
PSYoungGen total 9216K, used 7127K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 87% used [0x00000000ff600000,0x00000000ffcf5e28,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 32768K, used 0K [0x00000000fd600000, 0x00000000ff600000, 0x00000000ff600000)
object space 32768K, 0% used [0x00000000fd600000,0x00000000fd600000,0x00000000ff600000)
Metaspace used 2654K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 287K, capacity 386K, committed 512K, reserved 1048576K
0.080: [GC (Allocation Failure) [PSYoungGen: 7127K->680K(9216K)] 7127K->6832K(41984K), 0.0018474 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen total 9216K, used 680K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
from space 1024K, 66% used [0x00000000ffe00000,0x00000000ffeaa020,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 32768K, used 6152K [0x00000000fd600000, 0x00000000ff600000, 0x00000000ff600000)
object space 32768K, 18% used [0x00000000fd600000,0x00000000fdc02020,0x00000000ff600000)
Metaspace used 2654K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 287K, capacity 386K, committed 512K, reserved 1048576K
}
GC 日志描述
GC 日志的查看说明,也是网上找的,整理一下,以便后续查用。
[GC (Allocation Failure) [PSYoungGen: 7127K->736K(9216K)] 7127K->6888K(41984K), 0.0018287 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
以YGC来看,格式是
- [GC …][Times …]
- [GC [GC信息] GC前堆已用内存 -> GC后堆已用内存 (堆总内存), GC时间]
- [GC类型: GC前区域已用内存 -> GC后区域已用内存 (区域总内存)]
- [Times user 用户耗时 sys 系统耗时 real 实际耗时 ]
- [GC [GC信息] GC前堆已用内存 -> GC后堆已用内存 (堆总内存), GC时间]
上面的日志具体信息:分配内存失败,进行YGC。YGC 前新生代已用 7127 K,GC后 已用736K,总大小9216K(Eden + s0),堆(41984K)由7127K -> 6888K, YGC耗时 0.0018287秒。
Full GC 的日志
[Full GC (Ergonomics) [PSYoungGen: 704K->0K(9216K)] [ParOldGen: 30728K->31249K(32768K)] 31432K->31249K(41984K), [Metaspace: 2655K->2655K(1056768K)], 0.0068850 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
- [GC …][Times …]
- [GC [YGC信息][FGC信息] GC前堆已用内存 -> GC后堆已用内存 (堆总内存), [元空间回收信息], GC时间]
- [GC类型: GC前区域已用内存 -> GC后区域已用内存 (区域总内存)]
- [Times user 用户耗时 sys 系统耗时 real 实际耗时 ]
- [GC [YGC信息][FGC信息] GC前堆已用内存 -> GC后堆已用内存 (堆总内存), [元空间回收信息], GC时间]
FullGC: 新生代总大小 9216K,由704K ->0K;年老代大小 32768K,由30728K -> 31249K;元空间105678K,由2655K ->2655K。FGC耗时0.0068850秒,实际耗时约0.01秒
GC分析
根据GC日志可以看出,一共进行了 5YGC 3FGC 3YGC 1FGC。分析之前先看一下
GC悲观策略之Parallel GC篇
GC悲观策略之Serial GC篇
代码触发JVM的Full GC和Young GC
采用Parallel GC的情况下,当YGC触发时,会有两个检查:
1、在YGC执行前,min(目前新生代已使用的大小,之前平均晋升到old的大小中的较小值) > 年老代剩余空间大小 ? 不执行YGC,直接执行Full GC : 执行YGC;
2、在YGC执行后,平均晋升到old的大小 > 年老代剩余空间大小 ? 触发Full GC : 什么都不做。
新生代内存 8M 1M 1M, 年老代内存 31M
代码逻辑,for循环 往list中添加 3MB 的byte数组,因为数量级的原因,可忽略Byte和KB量级的对象。
循环 | E | s0 | s1 | old | gc |
---|---|---|---|---|---|
1 | 3 | 0 | 0 | 0 | / |
2 | 6 | 0 | 0 | 0 | / |
3 | 6 0+3 | 0 0 | 0 0 | 0 6 | YGC |
4 | 6 | 0 | 0 | 6 | / |
5 | 6 0+3 | 0 0 | 0 0 | 6 12 | YGC |
6 | 6 | 0 | 0 | 12 | / |
7 | 6 0+3 | 0 0 | 0 0 | 12 18 | YGC |
8 | 6 | 0 | 0 | 18 | / |
9 | 6 0+3 | 0 0 | 0 0 | 18 24 | YGC, old剩余7M, 大于6M,不触发FGC |
10 | 6 | 0 | 0 | 24 | / |
11 | 6 0+3 | 0 0 | 0 0 | 24 30 | YGC + FGC YGC 之后满足条件2,触发FGC |
查看前6次GC日志,Eden与Old的大小基本与实际的一致(忽略低量级对象)
之后的分析逻辑基本与上述过程类似,达成问题描述。
PS: 这只是针对 Parallel GC 的分析,帮助理解垃圾回收
思考
在实际编码过程中,大对象List或Map耗时操作时,使用完之后,可以及时清除掉。
这样即使所在方法作用域没有执行完成,GC的时候也可以自动回收大对象,否则一旦碰到极端情况,极易引起 old区占满,导致 OOM 问题。
引用
GC悲观策略之Parallel GC篇
GC悲观策略之Serial GC篇
代码触发JVM的Full GC和Young GC
GC总结