最近遇到一个 netty 的 OutOfDirectMemoryError 报错,是在分配 direct memory 时内存不足导致的,看了下报错提示,要分配的内存大小为 16M,剩余的空间不足。这里 max direct memory 大约有 7G,于是就有一个疑问,这个值是怎么设置的?
代码分析
这里使用的 netty 版本是 4.1.14.Final,如下是报错时的调用栈信息,主要关注下 PlatformDependent
这个类。
1 | Caused by: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 7532970287, max: 7549747200) |
找到 PlatformDependent
的第 572 行,位于 allocateDirectNoCleaner
函数内,它的功能是根据指定的容量(capacity)分配一个新的 ByteBuffer
1 | /** |
查看第 572 行对应的 incrementMemoryCounter
实现,它的功能是增加已使用内存的计数
1 | private static void incrementMemoryCounter(int capacity) { |
从代码逻辑可见,它是通过 CAS 更新已使用内存计数。在更新前先判断是否超过了 DIRECT_MEMORY_LIMIT
最大容量限制,若已超过则直接抛出异常,也就是说此时并未真正地分配内存。
这里就有个问题,DIRECT_MEMORY_LIMIT
是怎么设置的?
搜索代码发现,它是在 PlatformDependent
的静态代码块中设置的,代码如下:
1 | // Here is how the system property is used: |
首先取 io.netty.maxDirectMemory
属性值,根据它的不同取值有如下含义:
- < 0,不使用清理器(cleaner),从 java 继承 max direct memory 设置
- == 0,使用清理器(cleaner),netty 不会强制最大内存,而是使用 jdk 设置
- > 0,不使用清理器(cleaner),表示 netty 的最大 direct memory 限制
它的默认值是 -1,根据代码逻辑会执行到 maxDirectMemory = maxDirectMemory0()
这行,该方法的实现如下:
1 | private static final Pattern MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN = Pattern.compile( |
它的逻辑为:
- 通过反射调用
sun.misc.VM.maxDirectMemory()
,若取到则返回 - 否则,获取
-XX:MaxDirectMemorySize
配置,若取到则返回 - 否则,调用
Runtime.getRuntime().maxMemory()
获取
结论
根据以上的分析,要设置 direct memory 的最大容量,既可以通过 netty 的 io.netty.maxDirectMemory
属性配置,也可以通过 jvm 的 -XX:MaxDirectMemorySize
参数设置,其中前者的优先级更高。
默认情况下,上面两项均未配置,则是通过 sun.misc.VM.maxDirectMemory()
获取 direct memory 的最大容量。
测试
通过如下程序验证上面的结论
1 | public class DirectMemoryLimit { |
通过jvm设置
设置 jvm 参数 -XX:MaxDirectMemorySize=20m
,执行并查看结果
1 | directMemoryLimit: 20971520 byte |
其中 20M byte = 20 * 1024 * 1024 byte = 20971520 byte
通过netty设置
通过设置 io.netty.maxDirectMemory
属性,覆盖 -XX:MaxDirectMemorySize
配置的大小。
增加 jvm 参数 -Dio.netty.maxDirectMemory=1024
,执行并查看结果
1 | directMemoryLimit: 1024 byte |
去掉设置项
去掉上面的两个设置,再次执行并查看结果
1 | directMemoryLimit: 1908932608 byte |
增加如下代码
1 | System.out.println("maxMemory: " + Runtime.getRuntime().maxMemory()); |
执行并查看输出结果
1 | maxMemory: 1908932608 |
可见默认情况下,通过 sun.misc.VM.maxDirectMemory()
获取并设置的 DIRECT_MEMORY_LIMIT
取值与 Runtime.getRuntime().maxMemory()
一致
1 | public static void saveAndRemoveProperties(Properties var0) { |
Runtime.getRuntime().maxMemory()
这是一个 native 方法,从注释来看,它用于获取 java 虚拟机的最大可用内存。因为新生代里的 survivor 区采用的是复制算法,其可用空间只有一个 survivor 区大小,所以 java 堆总的可用空间大小为:老年代大小 + 新生代大小 - 一个 survivor 区大小
1 | /** |
到 jdk 源码找到该方法的 native 实现(jdk/src/share/native/java/lang/Runtime.c)
1 | JNIEXPORT jlong JNICALL |
它只是一个入口,具体实现在 hotspot 源码中(hotspot/src/share/vm/prims/jvm.cpp)
1 | JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void)) |
到 Universe.hpp
里找到 heap()
的实现。它返回了一个 CollectedHeap 类型的静态属性 _collectedHeap
,该静态属性是在 initialize_heap()
里初始化的。根据 GC 策略的不同,_collectedHeap
被初始化为不同的实现。
1 | // The particular choice of collected heap. |
打开 GenCollectedHeap
,找到 max_capacity()
的实现。它是将各个分代的最大容量相加。
1 | size_t GenCollectedHeap::max_capacity() const { |
剩下的源码还没搞清楚,有空再补。。。
参考
https://stackoverflow.com/questions/52980629/runtime-getruntime-maxmemory-calculate-method