同事团队在持续集成(CI)流程中将基础镜像从CentOSjava1.8
切换至AlmaLinuxjava1.8_302
后,发现部分应用出现OOM(Out Of Memory)异常。本文记录完整的问题排查过程与根因分析。
JDK OOM现场
问题现象
对比两种基础镜像环境观察到的异常现象:
特征项 | CentOS 环境 | AlmaLinux 环境 |
---|---|---|
Cgroup版本 | V1 | V2 |
进程内存消耗 | OldGen 最高 7.2GB | OldGen 最高 20.9GB |
Heap 配置 | 初始堆 576MB | 初始堆 2GB |
容器内存感知 | 正确识别 36GB | 错误识别宿主机 128GB |
OOM 发生率 | 无异常 | 高频出现 |
初步分析
- 进程与库文件分析
通过ps aux
和ldd
命令对比进程状态与动态链接库加载情况:
1 | # Alma 环境进程内存示例 |
动态链接库分析未发现异常so文件加载差异。
- JVM 配置对比
使用jinfo
验证关键JVM参数:
1 | # Metaspace 配置对比一致 |
关键配置对比表:
配置项 | CentOS 值 | AlmaLinux 值 |
---|---|---|
MaxHeapSize | 9.6GB | 32.2GB |
NewSize | 192MB | 682.5MB |
MaxNewSize | 3GB | 10.7GB |
OldSize | 384MB | 1.3GB |
最终发现:CentOS java1.8.0_302 根据 Pod Limit (通过挂载/proc/meminfo)计算,AlmaLinux java1.8.0_302 虽然同样挂载/proc/meminfo,但是还是根据宿主机取值。
技术背景
Cgroup V1与V2 针对OOM场景差异说明及监控优化
在 Linux 内存管理中,cgroup v1 的 rss
和 cgroup v2 的 anon
与 file
有一定的对应关系,但它们的内存统计方法和细节有所不同。以下是它们之间的关系及差异分析:
1. cgroup v1 的 rss
定义:
rss
表示进程实际使用的物理内存,主要包括:- 匿名内存(anon memory):进程的堆、栈,以及其他未与文件关联的内存。
- **活跃文件映射内存 **:由文件支持并被进程使用的内存(如共享库)。
- 特性:
- 只统计进程当前正在使用的物理内存(驻留在 RAM 中),不包括可以回收的页面缓存(如未使用的文件缓存)。
- 与进程的
RSS
概念一致,是不可回收的部分。
主要组成:
rss
= 匿名内存(anon
) + 活跃的文件映射内存(file
,但不包含页面缓存)。
2. cgroup v2 的 anon
和 file
- 定义:
- **
anon
**:匿名内存,包括进程的堆、栈,以及其他未映射到文件的内存区域。 - **
file
**:文件映射内存,包括文件页面缓存和进程正在使用的文件内存。 - 在 cgroup v2 中,内存统计进一步细化,将
anon
和file
分开,分别表示匿名内存和文件映射内存的使用量。
- **
- 特性:
- **
anon
**:始终表示 不可回收的匿名内存。 - **
file
**:可以包括两部分:- 活跃的文件映射内存:正在使用的文件支持内存(与 RSS 对应)。
- 页面缓存:文件数据的缓存部分,通常是可回收的。
- **
主要差异与优化
分类更细化:
- cgroup v1 中,
rss
直接合并了匿名内存和活跃文件映射内存,无法区分。 - cgroup v2 中,将
anon
和file
分开统计,明确区分匿名内存和文件相关内存。
- cgroup v1 中,
页面缓存的处理:
- 在 v1 中,页面缓存归入
cache
,不算在rss
中。 - 在 v2 中,
file
包含页面缓存,同时可以通过进一步分析分离出文件缓存部分。
- 在 v1 中,页面缓存归入
统计方法改进:
- cgroup v1 的
rss
偏粗粒度,无法具体判断匿名内存和文件映射内存的使用比例。 - cgroup v2 提供了更精细的指标,便于用户优化不同类型内存的使用。
- cgroup v1 的
Cgroup OOM场景对比
在 Cgroup v1 中:
- Cgroup的内存资源控制器限制每一个控制组的Page Cache和RSS物理内存.RSS 和 Cache 作为两种不同的内存使用类型,各自独立统计和管理。
- OOM触发必要条件:如果
RSS + Cache
超过了限制(memory.limit_in_bytes
),会触发内存限制。
在 Cgroup v2 中:
anon
和file
是两个主要的内存使用类别。- OOM触发必要条件:如果
anon + file
内存(即当前内存使用量memory.current)使用超过了memory.max
限制,会触发内存限制。
总结
- 在 cgroup v1 中,
rss
相当于 cgroup v2 的anon
+file
(活跃部分),但 v1 无法区分匿名内存和文件映射内存的详细信息。 - 在 cgroup v2 中,内存统计更清晰,
anon
和file
分别表示匿名内存和文件相关内存,后者还包括可回收的页面缓存。
JDK UseContainerSupport特性
早期 JDK 版本(< 8u191)无法感知容器资源限制:
- 根据宿主机内存设置堆大小 → 易触发 OOM
- 使用宿主机 CPU 核数 → 线程池配置不合理
核心功能
通过 -XX:+UseContainerSupport
启用(JDK 8u191+ / JDK 10+ 默认开启支持CgroupV1,JDK 8u372+ / JDK 11+ 支持CgroupV2):
内存适配
- 自动检测容器内存限制(如
docker run -m
) - 结合
-XX:MaxRAMPercentage=NN
按百分比分配堆内存
1
2# 示例:使用容器内存的 50%
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 ...- 自动检测容器内存限制(如
CPU 适配
- 自动识别容器 CPU 配额(如
docker --cpus
) - 用于优化并行处理(ForkJoinPool、GC 线程数等)
- 自动识别容器 CPU 配额(如
通过这一特性,Java 应用在容器环境中能更精准地利用分配资源,避免因资源超限导致的稳定性问题。
Linux E820
E820 是 x86 架构中用于获取系统物理内存布局的 BIOS 中断调用方法,属于计算机启动阶段内存管理的关键机制。Linux 内核通过调用 BIOS的中断来访问它,方法是将EAX 寄存器设置为十六进制值 E820(eax=E820)。它会报告哪些内存地址范围可用,哪些保留供 BIOS 使用,传统 BIOS 时代的主流内存探测方式(现代 UEFI 系统已转向新机制),Linux 内核仍支持解析 E820 表(如通过 dmesg | grep e820
查看),主要有几点应用场景:
- 操作系统启动时物理内存初始化
- 内存热插拔检测
- 虚拟化环境中客户机内存分配
- 嵌入式系统硬件资源管理
数据结构
通过 ES:DI
寄存器传递 内存条目结构体:
1 | C1struct e820_entry { |
类型码(Type):
1
:可用内存 (Usable RAM)2
:保留区域 (Reserved, 如 BIOS 或硬件占用)3
:ACPI 可回收内存4
:ACPI NVS 内存5
:坏内存 (Bad Memory)
工作流程
- 系统启动时:BIOS 初始化硬件后构建内存映射表
- 操作系统引导:通过
INT 15h, E820h
遍历所有内存区域 - 数据解析:内核筛选可用内存区域并建立页表
演变
- UEFI 替代:通过
GetMemoryMap
服务函数取代传统 BIOS 中断 - Linux 兼容:内核仍支持解析 E820 表(如通过
dmesg | grep e820
查看) - 虚拟化扩展:Hypervisor 可能虚拟化 E820 表供虚拟机使用
深入探究
JVM 内存计算机制
通过java -XX:+PrintFlagsFinal -version
输出对比发现关键差异:
1 | - uintx InitialHeapSize = 576MB # CentOS |
JVM 内存计算逻辑:
- 先判断是否容器内
a. 判断是否支持UseContainerSupport
b. 通过/proc/self/mountinfo
判断各子系统是否存在,我们在这里退出 - 不在容器内,则调用
Linux::physical_memory()
获取物理内存 - 物理内存:
_physical_memory = (julong)sysconf(_SC_PHYS_PAGES) * (julong)sysconf(_SC_PAGESIZE);
glibc 行为变更
关键 glibc PR(0ce657c5)变更:
1 | // CentOS glibc 2.17 |
导致容器内进程直接获取宿主机内存信息而非/proc/meminfo值。
sysinfo系统调用
_SC_PHYS_PAGES对应glibc函数为__get_phys_pages,其中使用glibc封装__sysinfo函数
__sysinfo对应linux内核sys_sysinfo系统调用,返回sysinfo结构体
sysinfo结构体中totalram字段为目标返回值:totalram_pages()
totalram_pages()返回字段为全局页面数目 _totalram_pages 变量
_totalram_pages变量在内核启动过程中
memblock 内存页帧分配器
填充,具体初始化细节如下:1
2
3
4
5
6start_kernel()
setup_arch()
e820__memory_setup()
memblock_set_current_limit(ISA_END_ADDRESS)
e820__memblock_setup()
...
总结
CentOS使用2.17 glibc通过读取/proc/meminfo获取内存信息,而AlmaLinux的2.34 glibc改用sysinfo系统调用,直接获取宿主机内存,导致JVM根据错误的内存值计算堆大小,进而引发OOM。glibc获取内存从 /proc/memory 变更为 sysinfo系统调用,sysinfo获取的值为宿主机内存大小,这与我们通过lxcfs挂载/proc/memory的行为存在错位.
短期方案
1
2# 强制指定堆内存参数
export JAVA_OPTS="-Xmx4g -Xms4g -XX:MaxRAMPercentage=75"长期方案
- 升级JDK版本至8u372
- 确保基础镜像Glibc版本一致性
参考
- https://www.cnblogs.com/aozhejin/p/17217813.html
- http://www.wowotech.net/memory_management/meminfo_1.html
- https://richardweiyang-2.gitbook.io/kernel-exploring/nei-cun-guan-li/00-memory_a_bottom_up_view/01-e820_retrieve_memory_from_hw
- https://tinylab.org/riscv-memblock/
- https://github.com/torvalds/linux/blob/feffde684ac29a3b7aec82d2df850fbdbdee55e4/mm/show_mem.c#L77
- https://zhuanlan.zhihu.com/p/508597947
- https://hwguo.github.io/blog/2015/05/31/linux-freemem/
- https://github.com/bminor/glibc/blob/776938e8b8dcf2b59998979e91cc0f9db7d771a8/sysdeps/unix/sysv/linux/getsysstats.c#L249
- https://cloud.tencent.com/developer/article/2352781
- https://bugs.openjdk.org/browse/JDK-8272124
- https://bugs.openjdk.org/browse/JDK-8230305
- https://github.com/bminor/glibc/commit/0ce657c576bf1b2436c4e14a002eaf461897d82c#diff-18d264a00a00688651894bf5a7ef0d80b9d75b33054b24e5adb12086deb416b7R317