Poison

MaxRAMPercentage

使用 -XX:MaxRAMPercentage 能够根据物理内存计算出最大堆大小,在容器服务等场景下相比 -Xmx 更易用。在 JDK 8 中计算相关源码位于:jdk8u/arguments.cpp at jdk8u352-b05 · openjdk/jdk8u · GitHub:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
void Arguments::set_heap_size() {
if (!FLAG_IS_DEFAULT(DefaultMaxRAMFraction)) {
// Deprecated flag
FLAG_SET_CMDLINE(uintx, MaxRAMFraction, DefaultMaxRAMFraction);
}

julong phys_mem =
FLAG_IS_DEFAULT(MaxRAM) ? MIN2(os::physical_memory(), (julong)MaxRAM)
: (julong)MaxRAM;

// Experimental support for CGroup memory limits
if (UseCGroupMemoryLimitForHeap) {
// This is a rough indicator that a CGroup limit may be in force
// for this process
const char* lim_file = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
FILE *fp = fopen(lim_file, "r");
if (fp != NULL) {
julong cgroup_max = 0;
int ret = fscanf(fp, JULONG_FORMAT, &cgroup_max);
if (ret == 1 && cgroup_max > 0) {
// If unlimited, cgroup_max will be a very large, but unspecified
// value, so use initial phys_mem as a limit
if (PrintGCDetails && Verbose) {
// Cannot use gclog_or_tty yet.
tty->print_cr("Setting phys_mem to the min of cgroup limit ("
JULONG_FORMAT "MB) and initial phys_mem ("
JULONG_FORMAT "MB)", cgroup_max/M, phys_mem/M);
}
phys_mem = MIN2(cgroup_max, phys_mem);
} else {
warning("Unable to read/parse cgroup memory limit from %s: %s",
lim_file, errno != 0 ? strerror(errno) : "unknown error");
}
fclose(fp);
} else {
warning("Unable to open cgroup memory limit file %s (%s)", lim_file, strerror(errno));
}
}

// Convert Fraction to Precentage values
if (FLAG_IS_DEFAULT(MaxRAMPercentage) &&
!FLAG_IS_DEFAULT(MaxRAMFraction))
MaxRAMPercentage = 100.0 / MaxRAMFraction;

if (FLAG_IS_DEFAULT(MinRAMPercentage) &&
!FLAG_IS_DEFAULT(MinRAMFraction))
MinRAMPercentage = 100.0 / MinRAMFraction;

if (FLAG_IS_DEFAULT(InitialRAMPercentage) &&
!FLAG_IS_DEFAULT(InitialRAMFraction))
InitialRAMPercentage = 100.0 / InitialRAMFraction;

// If the maximum heap size has not been set with -Xmx,
// then set it as fraction of the size of physical memory,
// respecting the maximum and minimum sizes of the heap.
if (FLAG_IS_DEFAULT(MaxHeapSize)) {
julong reasonable_max = (julong)((phys_mem * MaxRAMPercentage) / 100);
const julong reasonable_min = (julong)((phys_mem * MinRAMPercentage) / 100);
if (reasonable_min < MaxHeapSize) {
// Small physical memory, so use a minimum fraction of it for the heap
reasonable_max = reasonable_min;
} else {
// Not-small physical memory, so require a heap at least
// as large as MaxHeapSize
reasonable_max = MAX2(reasonable_max, (julong)MaxHeapSize);
}

if (!FLAG_IS_DEFAULT(ErgoHeapSizeLimit) && ErgoHeapSizeLimit != 0) {
// Limit the heap size to ErgoHeapSizeLimit
reasonable_max = MIN2(reasonable_max, (julong)ErgoHeapSizeLimit);
}
if (UseCompressedOops) {
// Limit the heap size to the maximum possible when using compressed oops
julong max_coop_heap = (julong)max_heap_for_compressed_oops();
if (HeapBaseMinAddress + MaxHeapSize < max_coop_heap) {
// Heap should be above HeapBaseMinAddress to get zero based compressed oops
// but it should be not less than default MaxHeapSize.
max_coop_heap -= HeapBaseMinAddress;
}
reasonable_max = MIN2(reasonable_max, max_coop_heap);
}
reasonable_max = limit_by_allocatable_memory(reasonable_max);

if (!FLAG_IS_DEFAULT(InitialHeapSize)) {
// An initial heap size was specified on the command line,
// so be sure that the maximum size is consistent. Done
// after call to limit_by_allocatable_memory because that
// method might reduce the allocation size.
reasonable_max = MAX2(reasonable_max, (julong)InitialHeapSize);
}

if (PrintGCDetails && Verbose) {
// Cannot use gclog_or_tty yet.
tty->print_cr(" Maximum heap size " SIZE_FORMAT, (size_t) reasonable_max);
}
FLAG_SET_ERGO(uintx, MaxHeapSize, (uintx)reasonable_max);
}

// If the minimum or initial heap_size have not been set or requested to be set
// ergonomically, set them accordingly.
if (InitialHeapSize == 0 || min_heap_size() == 0) {
julong reasonable_minimum = (julong)(OldSize + NewSize);

reasonable_minimum = MIN2(reasonable_minimum, (julong)MaxHeapSize);

reasonable_minimum = limit_by_allocatable_memory(reasonable_minimum);

if (InitialHeapSize == 0) {
julong reasonable_initial = (julong)((phys_mem * InitialRAMPercentage) / 100);

reasonable_initial = MAX3(reasonable_initial, reasonable_minimum, (julong)min_heap_size());
reasonable_initial = MIN2(reasonable_initial, (julong)MaxHeapSize);

reasonable_initial = limit_by_allocatable_memory(reasonable_initial);

if (PrintGCDetails && Verbose) {
// Cannot use gclog_or_tty yet.
tty->print_cr(" Initial heap size " SIZE_FORMAT, (uintx)reasonable_initial);
}
FLAG_SET_ERGO(uintx, InitialHeapSize, (uintx)reasonable_initial);
}
// If the minimum heap size has not been set (via -Xms),
// synchronize with InitialHeapSize to avoid errors with the default value.
if (min_heap_size() == 0) {
set_min_heap_size(MIN2((uintx)reasonable_minimum, InitialHeapSize));
if (PrintGCDetails && Verbose) {
// Cannot use gclog_or_tty yet.
tty->print_cr(" Minimum heap size " SIZE_FORMAT, min_heap_size());
}
}
}
}

需要注意的是 JDK 8 中的实现是基于 cgroup v1 的,如果内核使用的是 cgroup v2,则容器内存探测不会生效,当前使用的 cgroup 版本可以参考 Runtime metrics | Docker Documentation。关于 JDK 8 中基于 cgroup v2 的容器感知,可以参考 JDK-8230305 Cgroups v2: Container awareness - Java Bug System,可知,该 bug 在 JDK 15 中得到修复,并反向移植至了 JDK 11,但是 JDK 8 中并未进行反向移植,所以当使用 JDK 8 且使用了 -XX:MaxRAMPercentage 选项时,需要保证操作系统使用的 cgroup 版本为 v1,否则计算出的最大堆内存是基于物理机的内存,而不是对容器限制的内存。

我们可以用一台 Ubuntu 22.04 LTS 的机器来进行验证,首先确认物理机操作系统,执行 lsb_release -a 命令输出如下:

1
2
3
4
5
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04 LTS
Release: 22.04
Codename: jammy

执行 uname -a 确认内核版本如下:

1
Linux VM-0-16-ubuntu 5.15.0-40-generic #43-Ubuntu SMP Wed Jun 15 12:54:21 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

执行 free -h 确认物理机内存如下:

1
2
3
               total        used        free      shared  buff/cache   available
Mem: 3.3Gi 295Mi 671Mi 2.0Mi 2.4Gi 2.8Gi
Swap: 0B 0B 0B

可知物理机内存为 3.3G,我们再执行 ll /sys/fs/cgroup/cgroup.controllers 可确认该文件存在,表明当前操作系统使用的 cgroup 版本为 v2。

我们首先执行 sudo docker run -m 1GB openjdk:11 java -XX:MaxRAMPercentage=80.0 -XshowSettings:vm -version 验证 JDK 11 中对 cgroup v2 的支持,输出如下:

1
2
3
4
5
6
7
VM settings:
Max. Heap Size (Estimated): 792.69M
Using VM: OpenJDK 64-Bit Server VM

openjdk version "11.0.16" 2022-07-19
OpenJDK Runtime Environment 18.9 (build 11.0.16+8)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.16+8, mixed mode, sharing)

可知在容器中使用 JDK 11 时 JVM 实例的最大内存为我们设置的 1GB 的 80% 左右,说明探测生效。

我们再执行 sudo docker run -m 1GB openjdk:8 java -XX:MaxRAMPercentage=80.0 -XshowSettings:vm -version 验证 JDK 8 中对 cgroup v2 的支持,输出如下:

1
2
3
4
5
6
7
8
VM settings:
Max. Heap Size (Estimated): 2.58G
Ergonomics Machine Class: client
Using VM: OpenJDK 64-Bit Server VM

openjdk version "1.8.0_342"
OpenJDK Runtime Environment (build 1.8.0_342-b07)
OpenJDK 64-Bit Server VM (build 25.342-b07, mixed mode)

可知在容器中使用 JDK 8 时 JVM 实例的最大内存为 2.58G,并非我们指定的容器内存上限 1G 的 80% 左右,而是物理内存 3.3G 的 80% 左右,说明探测并未生效,此时读取到了物理机的内存。所以,在 JDK 8 下使用 -XX:MaxRAMPercentage 选项时需要注意操作系统使用的 cgroup 版本一定为 v1,否则最大堆内存限制不能按预期工作。

Reference

JVM Parameters InitialRAMPercentage, MinRAMPercentage, and MaxRAMPercentage | Baeldung
JDK-8146115 Improve docker container detection and resource configuration usage - Java Bug System
8146115: Improve docker container detection and resource configuratio… · openjdk/jdk8u@392e56e · GitHub
/sys/fs/cgroup/memory/memory.limit_in_bytes is missing in the container in version > 4.2.0 · Issue #6118 · docker/for-mac · GitHub