概述
最近我們公司在幫一個客戶查一個JVM的問題(JDK1.8.0_191-b12),發現一個系統老是被OS Kill掉,是記憶體泄露導致的,在查的程序中,陰差陽錯地發現了JVM另外的一個Bug,這個Bug可能會導致大量物理記憶體被使用,我們已經反饋給了社區,并得到快速反饋,預計在OpenJDK8最新版中發布(JDK11中也存在這個問題),
PS:用戶的那個問題最終也解決了,定位下來算是C2的一個設計缺陷導致大量記憶體被使用,安全性上沒有得到保障,
找出消耗大記憶體的執行緒
接下來主要分享下這個BUG的發現程序,先要客戶實時跟蹤行程的情況,當記憶體使用明顯上升的時候,通過/proc/<pid>/smaps,看到了不少64MB的記憶體分配,Rss也基本消耗完了,
7fd690000000-7fd693f23000 rw-p 00000000 00:00 0
Size: 64652 kB
Rss: 64652 kB
Pss: 64652 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 64652 kB
Referenced: 64652 kB
Anonymous: 64652 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd wr mr mw me nr sd
7fd693f23000-7fd694000000 ---p 00000000 00:00 0
Size: 884 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: mr mw me nr sd
再通過strace命令跟蹤了下系統呼叫,再回到上面的虛擬地址,我們找到了相關的mmap系統呼叫
[pid 71] 13:34:41.982589 mmap(0x7fd690000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7fd690000000 <0.000107>
執行mmap的執行緒是71號執行緒,接著通過jstack把執行緒dump出來,找到了對應的執行緒其實是C2 CompilerThread0
"C2 CompilerThread0" #39 daemon prio=9 os_prio=0 tid=0x00007fd8acebb000 nid=0x47 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
最后再grep了一下strace的輸出,果然看到這個執行緒在大量的進行記憶體分配,總共有2G多,
經典的64M問題
對于64M的問題,是一個非常經典的問題,在JVM中并沒有這種大量分配64M大小的邏輯,因此可以排除JVM特定意義的分配,這其實是glibc里針對malloc函式分配記憶體的一種機制,glibc從2.10開始提供的一種機制,為了分配記憶體更加高效,glibc提供了arena的機制,默認情況下在64位下每一個arena的大小是64M,下面是64M的計算邏輯,其中sizeof(long)為8
define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)
p2 = (char *) MMAP (aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE,
MAP_NORESERVE);
一個行程最多能分配的arena個數在64位下是8 * core,32位下是2 * core個
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
{
int n = __get_nprocs ();
if (n >= 1)
narenas_limit = NARENAS_FROM_NCORES (n);
else
/* We have no information about the system. Assume two
cores. */
narenas_limit = NARENAS_FROM_NCORES (2);
}
這種分配機制的好處,主要是應對多執行緒的環境,為每個核留有幾個64M的快取塊,這樣執行緒在分配記憶體的時候因為沒有鎖而變得更高效,如果達到上限了就會去慢速的main_arena里分配了,
可以通過設定環境變數MALLOC_ARENA_MAX來設定64M塊的個數,當我們設定為1的時候就會發現這些64M的記憶體塊都沒有了,然后都集中分配到一個大區域了,也就是main_arena,說明這個引數生效了,
無意的發現
再回過來思考為什么C2執行緒會出現大于2G的記憶體消耗的時候,無意中跟蹤C2這塊代碼發現了如下代碼可能會導致大量記憶體消耗,這個代碼的位置是nmethod.cpp的nmethod::metadata_do方法,不過這塊如果真的發生的話,肯定不是看到C2的執行緒大量分配,而是看到VMThread這個執行緒,因為下面這塊代碼主要是它執行的,
void nmethod::metadata_do(void f(Metadata*)) {
address low_boundary = verified_entry_point(); if (is_not_entrant()) {
low_boundary += NativeJump::instruction_size; // %%% Note: On SPARC we patch only a 4-byte trap, not a full NativeJump.
// (See comment above.) } { // Visit all immediate references that are embedded in the instruction stream.
RelocIterator iter(this, low_boundary); while (iter.next()) {
if (iter.type() == relocInfo::metadata_type ) {
metadata_Relocation* r = iter.metadata_reloc(); // In this metadata, we must only follow those metadatas directly embedded in
// the code. Other metadatas (oop_index>0) are seen as part of
// the metadata section below. assert(1 == (r->metadata_is_immediate()) +
(r->metadata_addr() >= metadata_begin() && r->metadata_addr() < metadata_end()), “metadata must be found in exactly one place”);
if (r->metadata_is_immediate() && r->metadata_value() != NULL) {
Metadata* md = r->metadata_value(); if (md != _method) f(md);
} } else if (iter.type() == relocInfo::virtual_call_type) {
// Check compiledIC holders associated with this nmethod CompiledIC *ic = CompiledIC_at(&iter); if (ic->is_icholder_call()) {
CompiledICHolder* cichk = ic->cached_icholder(); f(cichk->holder_metadata()); f(cichk->holder_klass()); } else {
Metadata* ic_oop = ic->cached_metadata(); if (ic_oop != NULL) {
f(ic_oop); } } } } }inline CompiledIC* CompiledIC_at(RelocIterator* reloc_iter) { assert(reloc_iter->type() == relocInfo::virtual_call_type ||
reloc_iter->type() == relocInfo::opt_virtual_call_type, "wrong reloc. info");
CompiledIC* c_ic = new CompiledIC(reloc_iter); c_ic->verify(); return c_ic;
}
注意上面的CompiledIC *ic = CompiledIC_at(&iter);這段代碼,因為CompiledIC是一個ResourceObj,這種資源會在c heap里分配(malloc),不過他們是和執行緒進行關聯的,假如我們在某處代碼宣告了ResourceMark,那當執行到這里的時候會標記當前的位置,再接下來執行緒要分配記憶體的時候如果執行緒關聯的記憶體不夠用,就會malloc一塊插進去并被管理起來,否則會實作記憶體的復用,當ResourceMark解構式執行的時候,會將之前的位置還原,后面這個執行緒如果要分配記憶體又會從這個位置開始復用記憶體塊,注意這里說的記憶體塊和上面的64M記憶體塊不是一個概念,
因為這段代碼在while回圈里,因此存在非常多次數的重復呼叫,這樣明明在執行完一次之后可以復用記憶體的地方并不能復用,而可能會導致大量的記憶體被不斷分配,表現起來可能就是物理記憶體消耗很大,遠大于Xmx,
這個修復辦法也很簡單,就是在CompiledIC *ic = CompiledIC_at(&iter);前加上ResourceMark rm;即可,
這個問題主要發生的場景是針對頻繁大量做Class Retransform或者Class Redefine的場景,所以如果系統里有這種agent的時候還是要稍微注意下這個問題,
這個問題發現后我們給社區提了patch,不過后面發現再JDK12中其實已經修復了,但是在之前的版本里的都沒有修復,這個問題提交給社區后,有人很快回應了,并可能在OpenJDK1.8.0-212中被fix,
最后在這里也簡單提下客戶那邊的那個問題,之所以C2執行緒消耗太大,最主要的原因是存在非常大的方法需要編譯,而這個編譯的程序是需要大量的記憶體消耗的,正因為如此,才會導致記憶體突然暴增,所以給大家一個建議,方法不要寫太大啦,如果這個方法呼叫還很頻繁,那真的會很悲劇的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/66114.html
標籤:其他
