啥?64KB?JVM老年代大小之谜

其实问题很简单,知识捡起来再看看~

每天凌晨我们会对集群中资源使用率较低的Pod进行配额调整,此时导致Pod重启,由于在凌晨没有多少业务流量,滚动更新对业务的影响也比较小。

但是, 这一天, 业务反馈出问题了。

问题现象

业务同学反馈,某个服务的部分容器异常,一直在重启,看了一下重启容器中JVM监控,发现一直在进行FullGC

于是也同步分析了一下GC信息,就看到了下面的结果。

看完就愣住了,老年代64KB是什么鬼。

问题分析

看到这里,首先想到的是看看启动时是不是配置的JVM参数不对。

启动时配置了xmsxmx,都是1GB。

但是结合前面的图就好像不对劲,GC日志分析的结果显示年轻代和老年大大小都对不上,于是实际登上容器jmap了一把。

那么问题来了,xmx配置了1GB,老年代+年轻代的大小总和也确实是1GB,但是两者的大小比例并不是我们熟知的2:1(NewRatio),也就意味着肯定还有地方配置了年轻代或老年代大小相关的参数。

于是顺手看一下进程启动的完整命令,就找到了问题点。

看上去在启动命令中,还有其他地方配置了xmsxmxxmn,而在最终执行的过程中,两次xmsxmx的配置会以后者为主,所以实际上的配置应该是:

diff 复制代码
-Xms1024M -Xmx1024M -Xmn1500M

那么问题来了,这种情况下的堆大小应该没有疑问的1024MB,但是年轻代和老年代大小是如何计算的呢?

年轻代取1500M,那不是超过了堆最大值? 年轻代如果不取1500M,到底应该设置多少? 那老年代会有多少空间?

要了解这些问题,需要我们顺着分代初始化的逻辑,一步一步透过源码来看看。

JVM堆分代初始化

分代初始化顺序

以常见的CMS为例(主要是其它的GC实现偷懒没细看,大体偏差不会太大,使用方式上略有不同),默认两分代的代码调用及分代初始化顺序如下。

整体流程上,

(1)Universe初始化JVM时,对堆进行初始化,调用不同GC策略的初始化过程;

(2)初始化过程中,initialize_flags逐级优先向上调用,初始化HeapSizeNewSizeOldSize的配置(最大、最小、当前值);

(3)再通过initialize_size_info逐级优先向上调用,初始化Heapgen0gen1的实际大小。

arduino 复制代码
这里的分代初始化顺序,单个分代的flag、size初始化顺序都很重要,直接影响最终的大小

看完整体的分代初始化顺序,我们单个分代逐个细看。

年轻代大小初始化

年轻代的初始化过程,从全局角度看其实分为三个阶段。

(1)基于用户配置读取并设置年轻代相关的配置(比如xmn),包含NewSizeMaxNewSize等;

(2)通过GenCollectorPolicy::initialize_flags(),重新计算年轻代大小的配置(NewSizeMaxNewSize),优先以用户配置为准,如无用户配置,则根据默认规则(NewRatio)进行大小配置的计算;

(3)通过GenCollectorPolicy::initialize_size_info(),最终计算年轻大的初始化大小(_min_gen0_size_initial_gen0_size_max_gen0_size)。

综上来看,年轻代的大小与xmnNewRatioNewSize、最大堆大小等都可能有关,最终完成年轻代的初始化值、最大/最小值的初始化。

年轻代最大值 ,优先以自定义的xmn为主,未设置时则以NewRatiomaxHeapSize来设置。
年轻代初始化值 ,分为两种情况,若xms=xmn,则年轻大的最大、最小和初始化值均计算出的max_new_size;若xmsxmn,则优先以NewSize进行初始化,否则根据堆大小及NewRatio按不同的情况进行配置。

老年代大小初始化

老年代初始化的逻辑与新生代差不多,不再赘述。

唯一有所不同的是,由于初始化老年代大小时,整体堆大小及新生代大小已经初始化完,如果老年代有通过启动参数OldSize手动指定老年代大小,则会进行一轮大小适配,避免堆及分代大小之前的冲突。

问题分析

回到问题本身,通过上面的内容,我们可以知道JVM堆内存初始化的顺序是heap_sizegen0_sizegen1_size,结合前面讨论的生效配置,我们逐步来看。

diff 复制代码
-Xms1024M -Xmx1024M -Xmn1500M

heap_size

首先初始化heap_size,按配置看来,heap的初始化、最大/最小值均为1024MB

gen0_size

其次是年轻代大小,按自定义配置为1500MB,但由于该大小已经超过heap_size,这种情况会怎么处理呢?

可以看看GenCollectorPolicy::initialize_size_info()中的处理过程~

MaxNewSize非默认值时,以MaxNewSize作为年轻代最大大小,且此时由于xms = xmx,所以init_gen0_sizemin_gen0_sizemax_gen0_size都会设置为MaxNewSize

MaxNewSize在哪配置的呢?答案在GenCollectorPolicy::initialize_flags()

综上来看,最终init_gen0_sizemin_gen0_sizemax_gen0_size最终都会设置为max_heap_size - {gen_aligenment}

gen1_size

那么问题来了,堆大小、年轻代大小都初始化完了,那老年代呢?

看看TwoGenerationCollectorPolicy::initialize_size_info()中是如何设置的。

此时老年代的初始化及最小值,与堆的初始化及最小值有关,代码转换后的逻辑,简单理解就是:

  1. heap_size足够大时,以heap_size - gen0_size作为gen1_size
  2. heap_size不够大时,至少保证gen1_size有一个分代对齐的大小{gen_alignment}

在我们的场景中会走到分支二,即gen1_size = {gen_alignment},最终得到下图结果,一个64KB的老年代。

复制代码
由于在计算gen0_size时,是取了heap_size - gen_alignment,此时gen1_size又设置为了gen_alignment,所以整体堆大小保持我们配置的xmx不变。

(只有老年代受伤的世界达成了「手动狗头」)

问题总结

综上来看,在确定堆大小后,如果新生代配置占用了太多堆空间,那么至少会保证老年代有一个分代对齐大小,即极端情况下的最小老年代大小,因此最终老年代大小只有64KB

一些关联的有趣问题

分代对齐大小

可以看待源码中,很多地方用到了gen_alignment做内存对齐,并在极端情况下,保证老年代大小不低于gen_alignment

关于gen_alignment的值,通过上文我们可以推测是64KB,那么它是在哪设置的呢?

兜兜转转是个圈,我们回到Universe中的initialize_heap()CMS垃圾收集器会调用initialize_all(),该函数如下:

所以在最初Universe初始化JVM堆及GC时,首先初始化了分代对齐大小,再更新flagsize

gen_alignment的设置过程如下: (分别在concurrentMarkSweepGeneration.hppgeneration.hpp中)

在非ARM平台下,GenGrain2^16,即64KB

objectivec 复制代码
重点!!!
以上内容为CMS中的实现,其他垃圾收集器可能不同,在Universe初始化堆的��现也拆分出了不同垃圾收集器的初始化逻辑。

Ergonomics

算是一个不算题外话的题外话 - Ergonomics

在设置大小的时候,经常会看到这样的配置:

在设置时,会通过一个特殊的宏定义来进行属性值设置,并在一些场合判断该值是否为某些特殊场景的设置值,实际定义共有这些标签类型:

其中,

  • DEFAULT,表示对应配置项为JVM默认设置;
  • ERGO ,即Ergonomics选项,表示JVM为了提供更好的用户体验而自动选择的设置;
  • CMDLINE,表示通过命令行参数传递给JVM的配置;
  • MGMT ,表示通过某种管理接口或工具进行设置的选项,例如JMX

JVM会在不同的场合,根据这些配置项的标签来做一些调整,比如某个配置项的标签CMDLINE时,JVM会优先以用户配置为准,不会再自动设置等等。

以上四种类型,从字面上最不好理解的应该就是Ergonomics

Ergonomics的说明,可以参考HotSpot Virtual Machine Garbage Collection Tuning GuideJDK8JDK11的说明中略有不同,大体思路一致,目的是在更少的命令行配置和调试下,以不同平台下的默认配置来满足不同场景下应用需求。

复制代码
Ergonomics是JVM和垃圾回收调优时的一种过程,用于提升应用性能,其提供与平台相关的垃圾收集器、堆大小、运行时编译器的默认配置。

其内容主要包含三大块,不作细讲,有兴趣可以看上面的官方原文,并不长。

大概的内容主要是以下两个部分: (此处以JDK11Ergonomics为例,原文还有一个part讲调优策略,就不展开了。)

  • 垃圾收集器、堆及运行时编译器的默认选项
  • 基于行为的调优策略

核心围绕两个目标:最大暂停时间、吞吐量。

一般情况下,满足了最大暂停时间,意味着更多次的GC,整体暂停时间会增加,带来的结果的一定吞吐量的下降;满足了吞吐量,意味着最大化GC的收益,单次GC可能会变成,但是整体暂停时间会减少。

这两个目标的值与MaxGCPauseMillisGCTimeRatio这两个配置有关,JVM会在满足了其中一个倾向达成的目标后,尝试最大化的达成另一个目标。两个目标要同时达成不容易,如果最大暂停时间及吞吐量目标均已满足,则垃圾收集器会减小堆大小,知道两个目标中的一个不满足为止。

相关推荐
一只叫煤球的猫2 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9652 小时前
tcp/ip 中的多路复用
后端
bobz9653 小时前
tls ingress 简单记录
后端
皮皮林5514 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友4 小时前
什么是OpenSSL
后端·安全·程序员
bobz9654 小时前
mcp 直接操作浏览器
后端
前端小张同学6 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook7 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康7 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在8 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net