承接上文
之前文章根據(jù)《【JVM故障問(wèn)題排查心得】「內(nèi)存診斷系列」JVM內(nèi)存與Kubernetes中pod的內(nèi)存椭住、容器的內(nèi)存不一致所引發(fā)的OOMKilled問(wèn)題總結(jié)(上)》我們知道了如何進(jìn)行設(shè)置和控制對(duì)應(yīng)的堆內(nèi)存和容器內(nèi)存的之間的關(guān)系,所以防止JVM的堆內(nèi)存超過(guò)了容器內(nèi)存,導(dǎo)致容器出現(xiàn)OOMKilled的情況立轧。但是在整個(gè)JVM進(jìn)程體系而言,不僅僅只包含了Heap堆內(nèi)存躏吊,其實(shí)還有其他相關(guān)的內(nèi)存存儲(chǔ)空間是需要我們考慮的氛改,一邊防止這些內(nèi)存空間會(huì)造成我們的容器內(nèi)存溢出的場(chǎng)景,正如下圖所示比伏。
接下來(lái)了我們需要進(jìn)行分析出heap之外的一部分就是對(duì)外內(nèi)存就是Off Heap Space胜卤,也就是Direct buffer memory堆外內(nèi)存。主要通過(guò)的方式就是采用Unsafe方式進(jìn)行申請(qǐng)內(nèi)存赁项,大多數(shù)場(chǎng)景也會(huì)通過(guò)Direct ByteBuffer方式進(jìn)行獲取葛躏。好廢話不多說(shuō)進(jìn)入正題。
JVM參數(shù)MaxDirectMemorySize
我們先研究一下jvm的-XX:MaxDirectMemorySize悠菜,該參數(shù)指定了DirectByteBuffer能分配的空間的限額舰攒,如果沒有顯示指定這個(gè)參數(shù)啟動(dòng)jvm,默認(rèn)值是xmx對(duì)應(yīng)的值(低版本是減去幸存區(qū)的大谢诖住)摩窃。
DirectByteBuffer對(duì)象是一種典型的”冰山對(duì)象”,在堆中存在少量的泄露的對(duì)象芬骄,但其下面連接用堆外內(nèi)存猾愿,這種情況容易造成內(nèi)存的大量使用而得不到釋放
-XX:MaxDirectMemorySize
-XX:MaxDirectMemorySize=size 用于設(shè)置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的單位可以使用 k/K德玫、m/M匪蟀、g/G;如果沒有設(shè)置該參數(shù)則默認(rèn)值為 0宰僧,意味著JVM自己自動(dòng)給NIO direct-buffer allocations選擇最大大小材彪。
-XX:MaxDirectMemorySize的默認(rèn)值是什么?
在sun.misc.VM中琴儿,它是Runtime.getRuntime.maxMemory()段化,這就是使用-Xmx配置的內(nèi)容。而對(duì)應(yīng)的JVM參數(shù)如何傳遞給JVM底層的呢造成?主要通過(guò)的是hotspot/share/prims/jvm.cpp显熏。我們來(lái)看一下jvm.cpp的JVM源碼來(lái)分一下。
// 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 命令參數(shù)轉(zhuǎn)換為 key 為 sun.nio.MaxDirectMemorySize的屬性晒屎。我們可以看出來(lái)他轉(zhuǎn)換為了該屬性之后喘蟆,進(jìn)行設(shè)置和初始化直接內(nèi)存的配置缓升。針對(duì)于直接內(nèi)存的核心類就在http://www.docjar.com/html/api/sun/misc/VM.java.html。大家有興趣可以看一下對(duì)應(yīng)的視線蕴轨。在JVM源碼里面的目錄是:java.base/jdk/internal/misc/VM.java港谊,我們看一下該類關(guān)于直接內(nèi)存的重點(diǎn)部分。
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;
上面可以看出來(lái)64MB最初是任意設(shè)置的橙弱。在-XX:MaxDirectMemorySize 是用來(lái)配置NIO direct memory上限用的VM參數(shù)歧寺。可以看一下JVM的這行代碼棘脐。
product(intx, MaxDirectMemorySize, -1,
"Maximum total size of NIO direct-buffer allocations")
但如果不配置它的話斜筐,direct memory默認(rèn)最多能申請(qǐng)多少內(nèi)存呢?這個(gè)參數(shù)默認(rèn)值是-1蛀缝,顯然不是一個(gè)“有效值”顷链。所以真正的默認(rèn)值肯定是從別的地方來(lái)的。
// 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蕴潦,那么則設(shè)置為 Runtime.getRuntime ().maxMemory ();如果有設(shè)置 MaxDirectMemorySize 且值大于 - 1俘闯,那么使用該值作為 directMemory 的值潭苞;而 VM 的 maxDirectMemory 方法則返回的是 directMemory 的值。
因?yàn)楫?dāng)MaxDirectMemorySize參數(shù)沒被顯式設(shè)置時(shí)它的值就是-1真朗,在Java類庫(kù)初始化時(shí)maxDirectMemory()被java.lang.System的靜態(tài)構(gòu)造器調(diào)用此疹,走的路徑就是這條:
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
}
而Runtime.maxMemory()在HotSpot VM里的實(shí)現(xiàn)是:
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
這個(gè)max_capacity()實(shí)際返回的是 -Xmx減去一個(gè)survivor space的預(yù)留大小。
結(jié)論分析說(shuō)明
MaxDirectMemorySize沒顯式配置的時(shí)候遮婶,NIO direct memory可申請(qǐng)的空間的上限就是-Xmx減去一個(gè)survivor space的預(yù)留大小蝗碎。例如如果您不配置-XX:MaxDirectMemorySize并配置-Xmx5g,則"默認(rèn)" MaxDirectMemorySize也將是5GB-survivor space區(qū)旗扑,并且應(yīng)用程序的總堆+直接內(nèi)存使用量可能會(huì)增長(zhǎng)到5 + 5 = 10 Gb 蹦骑。
其他獲取 maxDirectMemory 的值的API方法
BufferPoolMXBean 及 JavaNioAccess.BufferPool (通過(guò)SharedSecrets獲取) 的 getMemoryUsed 可以獲取 direct memory 的大小臀防;其中 java9 模塊化之后眠菇,SharedSecrets 從原來(lái)的 sun.misc.SharedSecrets 變更到 java.base 模塊下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 將其導(dǎo)出到 UNNAMED袱衷,這樣才可以運(yùn)行
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();
}
內(nèi)存分析問(wèn)題
-XX:+DisableExplicitGC 與 NIO的direct memory
用了-XX:+DisableExplicitGC參數(shù)后捎废,System.gc()的調(diào)用就會(huì)變成一個(gè)空調(diào)用,完全不會(huì)觸發(fā)任何GC(但是“函數(shù)調(diào)用”本身的開銷還是存在的哦~)致燥。
做ygc的時(shí)候會(huì)將新生代里的不可達(dá)的DirectByteBuffer對(duì)象及其堆外內(nèi)存回收了登疗,但是無(wú)法對(duì)old里的DirectByteBuffer對(duì)象及其堆外內(nèi)存進(jìn)行回收,這也是我們通常碰到的最大的問(wèn)題,如果有大量的DirectByteBuffer對(duì)象移到了old辐益,但是又一直沒有做cms gc或者full gc断傲,而只進(jìn)行ygc,那么我們的物理內(nèi)存可能被慢慢耗光智政,但是我們還不知道發(fā)生了什么艳悔,因?yàn)閔eap明明剩余的內(nèi)存還很多(前提是我們禁用了System.gc)。