👈👈👈 欢迎点赞收藏关注哟
首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一. 前言
之前学习完了 G1 回收器的一些特性和回收流程 ,这些都是一些偏理论性的东西。这一篇就来通过修改各项配置来看一下 G1 回收器在实际使用中的效果怎么样?
- 基础参数 :
-XX:+UseG1GC -Xms512m -Xmx2048m
二. 代码思路
这里使用的是 grafana + prometheus 进行观测。 同时结合 JVM 的相关组件进行一个观测。
个人 Demo 没有那么大的流量 ,同时为了效果更加清晰,我会灵活使用 强引用/软引用/弱引用达到我们测试的效果
-
强引用 : 和日常工作一样,日常我们申请的对象都是强引用。为了模拟对象不可达被回收的场景,这里
只有一半的强引用对象会被放入数组
-
软引用 : 当
全局内存不足的时候
,就会触发软引用的回收,释放内存 -
弱引用 :
一旦发生垃圾回收
,弱引用就会被直接回收
后续我会灵活利用这3种对象,达到我们期望的效果,使得概念的理解更加纯粹。
生产环境可能场景会更复杂,使用的时候要灵活的思考。
java
复制代码
public void get001() {
applicationContext.getBean(SessionController.class);
// 创建一个存储软引用的列表
List<SoftReference<byte[]>> softReferenceList = new ArrayList<>();
List<WeakReference<byte[]>> weakReferenceList = new ArrayList<>();
ArrayList<byte[]> retainedObjects = new ArrayList<>();
Random random = new Random();
// 分配内存并使用软引用引用这些内存块
while (true) {
int i = atomicInteger.addAndGet(1);
log.info("内存分配完成:{}", i);
if (i % 10 < 3) {
byte[] largeArray = new byte[(i % 10) * 1024 * 64]; // 64K - 0.64M
WeakReference<byte[]> weakReference = new WeakReference<>(largeArray);
weakReferenceList.add(weakReference);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else if (i % 10 < 5) {
// 以一定概率保留对象引用,防止它们被回收
byte[] largeArray = new byte[(i % 10) * 1024 * 64];
if (random.nextInt(10) < 5) {
retainedObjects.add(largeArray);
}
} else {
byte[] largeArray = new byte[(i % 10) * 1024 * 64];
SoftReference<byte[]> softReference = new SoftReference<>(largeArray);
softReferenceList.add(softReference);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
// 当内存满了 ,自动进行垃圾回收。
- G1 里面 Region 是 1M ,内存超过 50% 的 region 会直接纳入回收范围 ,
让对象不要过大
- 但是不同的场景我也会调整对象的大小,每个环节前面会特别标注
三. G1 性能指标
3.1 效果图如下
以上就是我们需要关注的核心指标 ,同时还顺带看一下 JMC 的核心数据。
3.2 我们应该关注哪些指标?
内存部分 :
- G1 Eden Space : Eden 区分配的内存大小
- G1 Survivor Space :Survivor 幸存者空间大小
- G1 Old Gen : 老年代分配的空间大小
垃圾回收次数部分 :
- end of major GC (Allocation Failure) - FullGC
- 当 JVM 需要分配内存但没有足够的空间
- end of minor GC (G1 Evacuation Pause) - YoungGC
- 将存活对象从 Eden 区域移动到 Survivor 区域或老年代
- end of minor GC (G1 Humongous Allocation) - YoungGC
- 大对象分配触发的 GC (大对象分配频繁 / 内存碎片化)
- end of minor GC (Metadata GC Threshold) - YoungGC
- 元数据(如类和方法的元数据)达到一定阈值时触发的垃圾回收
- 主要原因 : 类加载过多 / 元数据泄露 / Metaspace 分配过少
`这些变量的参数为 ops/s , 表示每秒执行的次数
垃圾回收时间部分 :
- avg end of major GC (Allocation Failure)
- 在因为内存分配失败而触发的全堆垃圾回收(Full GC)事件中,GC 暂停的平均时间
- avg end of minor GC (G1 Evacuation Pause)
- 在 G1 垃圾回收器的年轻代 GC(Evacuation Pause)事件中,GC 暂停的平均时间
- avg end of minor GC (G1 Humongous Allocation)
- 在由于大对象(Humongous Object)分配触发的年轻代 GC 事件中,GC 暂停的平均时间
- avg end of minor GC (Metadata GC Threshold)
- 在元数据(如类和方法)达到一定阈值时触发的年轻代 GC 事件中,GC 暂停的平均时间
- max end of major GC (Allocation Failure) : 下面四个和上面同理,区别在于最大时间
- max end of minor GC (G1 Evacuation Pause)
- max end of minor GC (G1 Humongous Allocation)
- max end of minor GC (Metadata GC Threshold)
四. 常见问题先分析
4.1 大对象 (10M)(只有软引用)
- 开局先来个纯粹的 ,
大对象是直接到老年代
,这个符合预期
-
Eden 区
used
数据 不太正常- 其主要原因也是由于大对象的问题
- 按照以往的分配规则 ,应该占三分之一才对,这里 明显是偏小了
-
Eden 区
committed
数据 不太正常- G1 回收器没有严格按照常规垃圾回收器的比例划分 ,而是基于当前状况做了动态调整
- 年轻代的 Region 最小 5% , 最大不超过 60% ,
这里应该没到 60% ,因为停顿时间要超
-
Survivor 区域就 相当不正常了
- 只有 1M 不到,基本上不经过处理直接就去了老年代
- 老年代相对大了一点 , FullGC 和 YoungGC 差不多频繁 , 这个
符合预期
,没年轻代嘛
阶段总结 :
- 这里出现这些问题一方面是软引用和大对象的特性导致,
内存不足才会回收,导致大部分对象会升级到老年代
。 - 其次是由于 G1 回收器没有任何配置,G1 偏向于智能处理 ,很容易出现奇怪的问题 ,所以线上还是要调优好哦
4.2 概念认知 :申请的内存和使用的内存
- 前置知识点一 : 虽然我们为应用分配了2G的内存,但是
一开始是不会直接给这么多内存的
- 就如上图 , 黄色为申请的内存 , 绿色为实际使用的内存
- init : JVM启动时从操作系统申请的初始内存 ,也就是 -Xms
- used : 实际使用的内存,包括未被垃圾回收期回收的不可达对象占用的内存
-
committed : 系统为 JVM 保留的内存 ,它
可以等于/大于 used , 可以小于 init
- 如果在分配对象的时候 ,used 内存可能操作 committed 时,就会香 JVM 申请这个值
- 但是如果该值无法再提高时,
且后续出现无法分配 used 内存的时候
,就会触发内存溢出
- max : JVM能从操作系统申请的最大内存
4.3 形态变化 : 申请 committed 内存的时机
- 可以明显看到 , 一开始内存 committed 是没有增长的 ,而在
老年代 used = committed
时 ,触发了 committed 的申请 - 后续 committed 内存会
逐步变多
,直到 committed 内存无法再申请,此时只有 used 内存会增长
4.4 内存溢出 : 我们是怎么把内存干爆的
- 现象一 :当最后达到某个临界点后 ,老年代 used 区域已经没有增长了 ,此时观测应用可以发现内存溢出
- 现象二 :每次 FullGC 后 ,有一批 Region 又被划归到 Eden 区域 ,虽然这批空间并没有被使用
- 新生代 / 老年代空间非线性变化
- 现象三 :虽然 FullGC 后区域被划给 Eden ,但是随着老年代可用空间减少 ,
Eden 区的最终空间越来越少
- 现象四 :committed 并没有被完全使用 ,
老年代不够了不会完全挤压新生代
(最小5%) ,当达到某个阈值的时候,系统就自然崩了
❓ 这里我其实一直不理解 ,为什么 used 还没有把 Committed 干满,为啥就溢出了 ?
其实想一想大概能推测出来 :
- G1 里面Eden 最小 5% , 拿走了100M ,
这些没办法再压缩了
- 老年代自己用了 1.5G ,加上各种类,元数据 ,拿走了一部分 ,
总共剩余 300M
- 再扣除碎片 ,大对象占用的一些废空间 ,实际剩余可用的空间可能就1-200M
- 重点 : 垃圾回收是要预留一部分空间去做移动和复制的 ,剩余的空间可能根本不够这些操作。 加上没有连续的内存区域 ,就可能直接触发内存溢出。
- 👉👉👉
仅推测 ,有其他的看法欢迎交流,这里我到现在都不太肯定
五. 实操效果 - 来了来了 ,他们来了
4.1 第一次尝试 : 修改引用类型
- 新的案例中,我将一半的对象转换成 弱引用,
弱引用的对象在垃圾回收时就会被处理
- 当我修改了
弱引用
后 ,由于弱引用
在垃圾回收时就会立即被处理 ,所以明显走到老年代的对象平缓了
- 还是一样的原因,由于
弱引用直接被收集了
,所以能到 Survivor 区的对象也几乎没有 - FullGC 的
频率
更多了 ,相对的效果就是 FullGC 的时间
明显变少 - G1 Humongous Allocation 大对象回收变得更加频繁了 (
原因未知
❗❗) - 总结 :
大对象对于GC的影响是很大的,由于几乎不受 YGC 的影响 ,使得GC变得不可控
4.2 第二次尝试 : 修改对象大小
- 上一轮中由于对象被设置得太大(10M) ,所以对象直接走了大对象回收而不是正常回收,所以继续优化 ,让每个对象大小不一样 👉
这里就可以着重关注下回收频率了,看是否和我们预期一样,大对象回收变少
。
java
复制代码
byte[] largeArray = new byte[(i % 10) * 1024 * 512];
- 老年代成长的曲线更流畅了 ,回收频率也变得更加正常
- 因为之前基本上都是大对象,直接入了老年代
- 现在不会突然有大对象干满老年代内存,小对象停顿时间更加可控,回收自然更加流畅
-
年轻代回收的频率明显变高了
(因为小对象变多了,可以在年轻代被回收) - 但是由于还有弱引用,且对象大小大部分还是超过了1M ,所以
效果还是不明显
- 出于引用类型的特性 ,Survivor 区还是个空~~
4.3 第三次尝试 : 让对象更加微小
在这个环节,我们会少量加入强引用对象
,同时移除弱引用的影响 ,保留软引用(避免直接内存溢出
).
同时调整对象大小 ,由64K 到 0.5M 不等,且强引用都是小对象 ,来 ,继续 :
java
复制代码
byte[] largeArray = new byte[(i % 10) * 1024 * 64];
这里信息简直太多了,我们一个个来分析
S1 : 年轻代充实起来了
- 可以看到 ,相比之前的尝试 ,Eden 区 和 Survivor 区明显更加充实了 ,比例也是健康的 8:1:1
- 由于对象更加可控,所以老年代 committed 和 used 更加接近,
内存利用率最大化
S2 : 总内存在阶梯式申请 ,形态开始发生变化
- 每到
committed
内存不足时,就会阶梯式的申请一段内存
- 随着老年代内存的增长 ,
新生代的内存空间也在动态变化
- 同时基于
目标暂停时间
, 相关的参数也会变化(下一期讨论) ,所以这里的回收时间每次变化的时候都很长
但是这个环节对象大小太小了,而且强引用仅占十分之一,所以一直每触发 FullGC.
各种类型和大小的对象对 GC 的影响都分析过了,下面来看一版贴近生产的
4.4 最终版本 ,贴近生产
- 这一版
停顿时间
为 200ms ,强引用的数量会较多 ,同样加入了弱引用模拟用完的对象
- 由于调低了停顿时间, 所以年轻代回收的频率也有所变高
- 其他的和预期的线上环节区别不大 ,老年代挤压新生代空间
- 新生代虽然每次 FullGC 后都有大量的空间,但是时间的要求 ,每次还没变大就被回收了
4.5 最终会演变成什么 ?
可能有人会问 ,为什么一开始没有使用这种方式 ,其实原因很简单 :内存会泄露, 因为老年代会越来越多,而且无法清除。当所有的软引用都被清除后 ,剩下的就是无法清除的对象了。
我们把上文代码里面的取模调大点,让大部分对象都是强引用,就会触发如下效果 :
4.6 阶段总结
在不优化配置的情况下 ,对象的类型和大小直接影响了垃圾回收的效果。
所以一般情况下 ,不要在系统里面使用大对象,对垃圾回收的负荷是很大的。
另外要格外的注意 ,为了让这个过程变得更加可控,上文我使用的只有软引用和弱引用。他们俩一个碰到回收就被收走了,一个是空间没了才会被收走
。这种其实不符合生产的业务场景,所以对代码做一些改进 :
总结
本来想一起做的配置影响这一篇是上不了了, 虽然看起来内容不是很多 ,但是实打实的花了2天时间。
一个是跑数据不能太快,不然不好去分析,再一个遇到问题查资料也很花时间。
注意 : 这些数据都是没有进行调优时得到的结果 , 目的是为了现象更纯粹,所以有些点不能
按照生产的结果直接往上面套 ,注意灵活思考
。
还有一些问题不确定, 后续会继续深入,如果结论有意思,还会单独发出来。
大家有看法也可以提出宝贵的意见,想不通,真的想不通。
如有侵权请联系站点删除!
技术合作服务热线,欢迎来电咨询!