写点什么

记一次 Jvm 参数调优实战

用户头像
AI乔治
关注
发布于: 2020 年 10 月 27 日
记一次Jvm参数调优实战

案例一

public class test1 {    private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IOException, InterruptedException { System.out.println("My Process Id is:"+getProcessID()); Thread.sleep(10000); byte[] all1 = new byte[ 2 * _1MB]; byte[] all2 = new byte[ 2 * _1MB]; Thread.sleep(2000); byte[] all3 = new byte[ 2 * _1MB]; byte[] all4 = new byte[ 7 * _1MB]; System.in.read(); } public static int getProcessID() { RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); return Integer.valueOf(runtimeMXBean.getName().split("@")[0]) .intValue(); }}复制代码
复制代码

注:这里 getProcessId 的作用是拿到进程号

jvm 参数

-Xmx20m  //   设置最大堆大小-Xms20m   // 设置最小堆大小,一般和-Xmx一致-Xmn10m   //  设置新生代大小-XX:+UseParNewGC   //表示新生代使用ParNewGc-XX:+UseConcMarkSweepGC   // 表示老年代使用CMS-XX:+UseCMSInitiatingOccupancyOnly   //表示CMS不基于运行时收集数据来进行GC控制-XX:CMSInitiatingOccupancyFraction=75   //而表示当老年代使用率到达阈值75%时触发复制代码
复制代码

我们这么设置 JVM 参数,就可以看出一些基本设置:

  • 年轻代 10M

  • 老年代 10M

  • eden:s0:s1 = 8:1:1

  • 新生代使用 ParNewGc

  • 老年代使用 CMS,并只有当老年代使用率超过 75 的时候触发 FullGC

我们先简单看一下这么设置有什么问题:

代码里先创建了 2M 的对象,直接放入 eden 区,再创建了 2M 的对象,同样也放入 eden 区,此时 eden 有 4M 的对象,再创建了 2M 的对象,eden 有 4M,可以放更多,这 2M 也放进了 eden,最后创建了 7M 对象,eden 区存不下,所以会触发一次 young gc,但是剩下的 s0,s1 已经放不下,所以放入老年代,此时 eden 有 7M 对象,老年代有 6M 对象,此时年轻代和老年代都有剩下的空间,不会触发 GC,但,真的是这样吗?

我们实战看看:,在程序开始时,记录下输出的 pid,然后 在程序运行的饿时候在 cmd 使用 jstat -gcutil pid 1000 ,每一秒输出一次 gc 信息,看看结果是什么,先在代码上每一处加上 sleep 为了方便观察



第一个绿线,是第一个 2M 对象生成,此时 eden 区使用了 63%,6.3M 的空间

第二个绿线,是第二个 2M 对象生成,此时 eden 区使用了 88%,8.8M 空间

第三个绿线,是第三个 2M 对象生成,此时 eden 区并不足够承载这个 2M 对象,

此时进行 young GC,如图 YGC 被触发一次,但是现存的 4MB 对象均不能被回收,且大于 S0、S1 的空间,所以直接进入老年代,此时老年代有 4M 的空间,正好是那两个 2M 的对象,此时 Eden 区已经可以存放 2M,此时 eden 去和 s0 一起 young gc 可以放入 s1 的对象即 1M 左右的零碎对象放入了 S1,然后 s1、s0 名称互换 。

而且,第一次 young gc 后,消失了部分空间,这即是 “垃圾"", 被回收掉了,但是为什么 Metespace 区容量暴增呢?

第四个红线,是第四个 7M 对象生成,eden 区并不能存放这个 7M 的大对象,则需要进行一次 younggc,s1 中的 100%的 1M 的对象被垃圾回收部分垃圾并放到了 s0 区,同样此时 eden 区的 2.5M 有一个 2M 的对象,此时没有办法进入 s0 s1,所以只能进入老年代,此时老年代占用 70%,而 eden 占用 90%,并不具备 full gc 触发条件,当时 full GC 被触发,

且每隔两秒被触发一次?这是为什么?

我们再这个 jstat 上已经无法得出更重要的信息,我们打印 GC 日志看看,

在启动参数上加上

-XX:+PrintGCDetails  // 打印详细日志-XX:+PrintHeapAtGC   // 在GC前后打印堆信息-XX:+PrintGCDateStamps   // 打印时间复制代码
复制代码

我们启动后,看看日志输出:



很容易发现,在第三个 2MB 分配时,进行了一次 GC,在 GC 前堆的信息为:

eden:88%, s0 0%,s1 0% CMS 区 0%,元空间区 0%,类空间 0%

此时日志里写到 Allocation Failure ,即空间分配失败,然后后面 PerNew 的空间由 7261k-》1023K,即进行一次 young GC,此时年轻代空间总大小为 9216K,为啥不是 10240k?因为这里算得空间是 eden+s1

然后看 gc 后的堆的空间信息:eden 为 0,即所有的对象都被移走了,移到哪了?4M 的大对象移到了堆,剩下的进行垃圾回收只剩下 1023k 移入到了 from 也就是 s1 区

此时老年代有 4117k,即 4M 的大对象

然后 GC 后就是分配刚刚的 2MB 的对象到 eden,如下图刚开始就有 26%的 used,就是这个 2MB 的对象

然后后面就是分配一个 7M 的大对象



在分配 7MB 大对象时,进行了一次 young GC,同样看 GC 日志 ,perNew 由 3217K-》102K 然后看 GC 后的堆空间情况:eden:0,s1:9%,CMS 区 7219k,72%

  • 到这里就和上面的 jstat 的信息保持一致,那么主要关键点在下面



剩下的未截出的都是完全重复的信息

这些记录里记录了什么?

首先是 CMS 初始标记 CMS Initial Mark,此时 STW

然后进入并发标记 CMS-concurrent-mark-start

并在并发标记中执行并发预清理 CMS-concurrent-preclean-start,由于此时并没有对象能被清理,所以此处无效,也没有消耗时间

并发预清理主要做两件事:

处理新生代已经发现的引用

如果老年代中有对象内部引用发生变化,会把所在的 Card 标记为 Dirty

在执行预清理过程中,有个可中断的预清理 CMS-concurrent-abortable-preclean-start,这里主要做两件事:

处理 From 和 To 区的对象,标记可达的老年代对象

扫描处理 Dirty Card 中的对象

最后执行 重新标记 Final Remark,STW

然而后面就没有数据了,后面又是重复的进行 CMS 的扫描,没有执行清理,同样也没有执行 Full GC

那么上面的 Full GC 又是怎么来的呢?

根据资料显示:这是 CMS 的一个并行收集阶段,只有到达阈值 (75)才进行清除

之前的标记信息都是并行收集阶段,走这一步在 JVM 监控中识别成了一次 Full GC,实则并不是!

破案了,所以这并不是 2 秒一次 Full GC,而是 2 秒一次 CMS 的并行搜集!

案例二

同样是上面的例子,例子来源上说当不用 CMS 时,不会触发,原因是什么?(88 当然是没有并行收集这一阶段,直接没有 Full gc 信息**)

案例三

同样是上面例子,当接着再次生成一个 8M 的对象,会发生什么?

直接 OOM,因为在生成之前,eden 最大也 8M,存不下这个 8M,然后老年代也存不下,直接抛出错误

经测试,大于等于 4M 的大对象在此时创建,均会 OOM

此时为:eden 区已经被使用了 89%,老年代也被使用了 6817k,完全不足以支撑下个对象,所以 OOM

那么这个最大 4M 是怎么得来的?此时 eden 只有一个 7M 的大对象,此时这个 7M 的大对象并不能放入老年代,所以此时放入堆中的对象只有:小于 eden 区的剩余空间,1M,或者小于老年代的剩余空间,即 4M,所以这个 4,就是这么来的

还是看看 GC 日志把



当分配第五个 7M 的前后,为什么 eden 区突然有了 89%的占用率?其实这就是上一个 GC 后释放了空间,释放的空间就在 GC 后显示,然后才是这个第四个 7M 分配的空间,所以这个 89%,就是第 4 个 7M 分配的空间



在分配第五个 7M 的时候,eden 区放不下,所以先进行一次 young gc,但是此时年轻代存的都是存活的大对象,无法被回收,所以这个年轻代 GC 并没有回收任何一点垃圾 然后后面再回进行一次 GC,即 FullGC



这个 FullGC 一共回收了俩地方,一个 CMS,一个 matespace(后面没截出来 此时 CMS 经过 Full GC 后仍然有 6781K,而 matespace 就一点没小,所以此时老年代无法分配空间, 这里不 youngGC 是因为前面已经 YoungGC 过了, 此时年轻代老年代都不足以分配空间给新对象,所以 OOM

案例四

还是上面的例子,如果有两种情况,一种是第 4 个 7M 接下来分配一个小于 1M 的对象,这个对象会被分配到哪?如果是大于 2M 小于 4M 呢?

先来分配一个 1M 的对象(小于当前 eden 区的剩余空间



此时 eden 只有 11%的剩余 1.1M,可以承载 1M 的对象,但是超过了阈值(95)触发 young gc

young gc 过后,将其放入 eden,加上一些空间碎片,此时 eden 已经 100%,再次歘 young gc,

此时 eden 有俩对象,一个 7M,一个 1M,就只能将 1M 的对象放入老年代,此时 eden 占用了 90%的空间

我们再来分配一个 3M 的对象,此时 eden 同样无法接受 3M 对象,先执行一次 young gc,然后仍然无法存储,但是老年代空间够,所以直接移到老年代去,此时老年代有 90%的占用率,但是后面为什么没有触发 Ful gc?(这里省略了图)

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java 烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章 ing🚀



作者:Crush-718637

出处:https://club.perfma.com/article/1956895


用户头像

AI乔治

关注

分享后端技术干货。公众号【 Java烂猪皮】 2019.06.30 加入

一名默默无闻的扫地僧!

评论

发布
暂无评论
记一次Jvm参数调优实战