承接上文
之前文章根据《【JVM 故障问题排查心得】「内存诊断系列」JVM 内存与 Kubernetes 中 pod 的内存、容器的内存不一致所引发的 OOMKilled 问题总结(上)》我们知道了如何进行设置和控制对应的堆内存和容器内存的之间的关系,所以防止 JVM 的堆内存超过了容器内存,导致容器出现 OOMKilled 的情况。但是在整个 JVM 进程体系而言,不仅仅只包含了 Heap 堆内存,其实还有其他相关的内存存储空间是需要我们考虑的,一边防止这些内存空间会造成我们的容器内存溢出的场景,正如下图所示。
接下来了我们需要进行分析出 heap 之外的一部分就是对外内存就是 Off Heap Space,也就是 Direct buffer memory 堆外内存。主要通过的方式就是采用 Unsafe 方式进行申请内存,大多数场景也会通过 Direct ByteBuffer 方式进行获取。好废话不多说进入正题。
JVM 参数 MaxDirectMemorySize
我们先研究一下 jvm 的-XX:MaxDirectMemorySize,该参数指定了 DirectByteBuffer 能分配的空间的限额,如果没有显示指定这个参数启动 jvm,默认值是 xmx 对应的值(低版本是减去幸存区的大小)。
DirectByteBuffer 对象是一种典型的”冰山对象”,在堆中存在少量的泄露的对象,但其下面连接用堆外内存,这种情况容易造成内存的大量使用而得不到释放
-XX:MaxDirectMemorySize=size 用于设置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的单位可以使用 k/K、m/M、g/G;如果没有设置该参数则默认值为 0,意味着 JVM 自己自动给 NIO direct-buffer allocations 选择最大大小。
-XX:MaxDirectMemorySize 的默认值是什么?
在 sun.misc.VM 中,它是 Runtime.getRuntime.maxMemory(),这就是使用-Xmx 配置的内容。而对应的 JVM 参数如何传递给 JVM 底层的呢?主要通过的是 hotspot/share/prims/jvm.cpp。我们来看一下 jvm.cpp 的 JVM 源码来分一下。
// Convert the -XX:MaxDirectMemorySize= command line flag
// to the sun.nio.MaxDirectMemorySize property.
// Do this after setting user properties to prevent people
// from setting the value with a -D option, as requested.
// Leave empty if not supplied
if (!FLAG_IS_DEFAULT(MaxDirectMemorySize)) {
char as_chars[256];
jio_snprintf(as_chars, sizeof(as_chars), JULONG_FORMAT, MaxDirectMemorySize);
Handle key_str = java_lang_String::create_from_platform_dependent_str("sun.nio.MaxDirectMemorySize", CHECK_NULL);
Handle value_str = java_lang_String::create_from_platform_dependent_str(as_chars, CHECK_NULL);
result_h->obj_at_put(ndx * 2, key_str());
result_h->obj_at_put(ndx * 2 + 1, value_str());
ndx++;
}
复制代码
jvm.cpp 里头有一段代码用于把 - XX:MaxDirectMemorySize 命令参数转换为 key 为 sun.nio.MaxDirectMemorySize 的属性。我们可以看出来他转换为了该属性之后,进行设置和初始化直接内存的配置。针对于直接内存的核心类就在http://www.docjar.com/html/api/sun/misc/VM.java.html。大家有兴趣可以看一下对应的视线。在 JVM 源码里面的目录是:java.base/jdk/internal/misc/VM.java,我们看一下该类关于直接内存的重点部分。
public class VM {
// the init level when the VM is fully initialized
private static final int JAVA_LANG_SYSTEM_INITED = 1;
private static final int MODULE_SYSTEM_INITED = 2;
private static final int SYSTEM_LOADER_INITIALIZING = 3;
private static final int SYSTEM_BOOTED = 4;
private static final int SYSTEM_SHUTDOWN = 5;
// 0, 1, 2, ...
private static volatile int initLevel;
private static final Object lock = new Object();
//......
// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory. This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;
复制代码
上面可以看出来 64MB 最初是任意设置的。在-XX:MaxDirectMemorySize 是用来配置 NIO direct memory 上限用的 VM 参数。可以看一下 JVM 的这行代码。
product(intx, MaxDirectMemorySize, -1,
"Maximum total size of NIO direct-buffer allocations")
复制代码
但如果不配置它的话,direct memory 默认最多能申请多少内存呢?这个参数默认值是-1,显然不是一个“有效值”。所以真正的默认值肯定是从别的地方来的。
// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
return directMemory;
}
//......
// Save a private copy of the system properties and remove
// the system properties that are not intended for public access.
//
// This method can only be invoked during system initialization.
public static void saveProperties(Map<String, String> props) {
if (initLevel() != 0)
throw new IllegalStateException("Wrong init level");
// only main thread is running at this time, so savedProps and
// its content will be correctly published to threads started later
if (savedProps == null) {
savedProps = props;
}
// Set the maximum amount of direct memory. This value is controlled
// by the vm option -XX:MaxDirectMemorySize=<size>.
// The maximum amount of allocatable direct buffer memory (in bytes)
// from the system property sun.nio.MaxDirectMemorySize set by the VM.
// If not set or set to -1, the max memory will be used
// The system property will be removed.
String s = props.get("sun.nio.MaxDirectMemorySize");
if (s == null || s.isEmpty() || s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
} else {
long l = Long.parseLong(s);
if (l > -1)
directMemory = l;
}
// Check if direct buffers should be page aligned
s = props.get("sun.nio.PageAlignDirectMemory");
if ("true".equals(s))
pageAlignDirectMemory = true;
}
//......
}
复制代码
从上面的源码可以读取 sun.nio.MaxDirectMemorySize 属性,如果为 null 或者是空或者是 - 1,那么则设置为 Runtime.getRuntime ().maxMemory ();如果有设置 MaxDirectMemorySize 且值大于 - 1,那么使用该值作为 directMemory 的值;而 VM 的 maxDirectMemory 方法则返回的是 directMemory 的值。
因为当 MaxDirectMemorySize 参数没被显式设置时它的值就是-1,在 Java 类库初始化时 maxDirectMemory()被 java.lang.System 的静态构造器调用,走的路径就是这条:
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
}
复制代码
而 Runtime.maxMemory()在 HotSpot VM 里的实现是:
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
JVMWrapper("JVM_MaxMemory");
size_t n = Universe::heap()->max_capacity();
return convert_size_t_to_jlong(n);
JVM_END
复制代码
这个 max_capacity()实际返回的是 -Xmx 减去一个 survivor space 的预留大小。
结论分析说明
MaxDirectMemorySize 没显式配置的时候,NIO direct memory 可申请的空间的上限就是-Xmx 减去一个 survivor space 的预留大小。例如如果您不配置-XX:MaxDirectMemorySize 并配置-Xmx5g,则"默认" MaxDirectMemorySize 也将是 5GB-survivor space 区,并且应用程序的总堆+直接内存使用量可能会增长到 5 + 5 = 10 Gb 。
其他获取 maxDirectMemory 的值的 API 方法
BufferPoolMXBean 及 JavaNioAccess.BufferPool (通过 SharedSecrets 获取) 的 getMemoryUsed 可以获取 direct memory 的大小;其中 java9 模块化之后,SharedSecrets 从原来的 sun.misc.SharedSecrets 变更到 java.base 模块下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 将其导出到 UNNAMED,这样才可以运行
public BufferPoolMXBean getDirectBufferPoolMBean(){
return ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
.stream()
.filter(e -> e.getName().equals("direct"))
.findFirst()
.orElseThrow();
}
public JavaNioAccess.BufferPool getNioBufferPool(){
return SharedSecrets.getJavaNioAccess().getDirectBufferPool();
}
复制代码
内存分析问题
-XX:+DisableExplicitGC 与 NIO 的 direct memory
用了-XX:+DisableExplicitGC 参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何 GC(但是“函数调用”本身的开销还是存在的哦~)。
做 ygc 的时候会将新生代里的不可达的 DirectByteBuffer 对象及其堆外内存回收了,但是无法对 old 里的 DirectByteBuffer 对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题,如果有大量的 DirectByteBuffer 对象移到了 old,但是又一直没有做 cms gc 或者 full gc,而只进行 ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为 heap 明明剩余的内存还很多(前提是我们禁用了 System.gc)。
评论