0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Java進(jìn)程所使用的內(nèi)存情況介紹

openEuler ? 來源:openEuler ? 作者:openEuler ? 2022-10-12 11:51 ? 次閱讀

0.引言

我們經(jīng)常會(huì)好奇,我啟動(dòng)了一個(gè) JVM,他到底會(huì)占據(jù)多大的內(nèi)存?他的內(nèi)存都消耗在哪里?為什么 JVM 使用的內(nèi)存比我設(shè)置的 -Xmx 大這么多?我的內(nèi)存設(shè)置參數(shù)是否合理?為什么我的 JVM 內(nèi)存一直緩慢增長(zhǎng)?為什么我的 JVM 會(huì)被 OOMKiller 等等,這都涉及到 JAVA 虛擬機(jī)對(duì)內(nèi)存的一個(gè)使用情況,不如讓我們來一探其中究竟。

1.簡(jiǎn)介

除去大家都熟悉的可以使用 -Xms、-Xmx 等參數(shù)設(shè)置的堆(Java Heap),JVM 還有所謂的非堆內(nèi)存(Non-Heap Memory)。

可以通過一張圖來簡(jiǎn)單看一下 Java 進(jìn)程所使用的內(nèi)存情況(簡(jiǎn)略情況):

b309c9c2-494c-11ed-a3b6-dac502259ad0.png

非堆內(nèi)存包括方法區(qū)和Java虛擬機(jī)內(nèi)部做處理或優(yōu)化所需的內(nèi)存。

方法區(qū):在所有線程之間共享,存儲(chǔ)每個(gè)類的結(jié)構(gòu),如運(yùn)行時(shí)常量池、字段和方法數(shù)據(jù),以及方法和構(gòu)造函數(shù)的代碼。方法區(qū)在邏輯上(虛擬機(jī)規(guī)范)是堆的一部分,但規(guī)范并不限定實(shí)現(xiàn)方法區(qū)的內(nèi)存位置和編譯代碼的管理策略,所以不同的 Java 虛擬機(jī)可能有不同的實(shí)現(xiàn)方式,此處我們僅討論 HotSpot。

除了方法區(qū)域外,Java 虛擬機(jī)實(shí)現(xiàn)可能需要內(nèi)存用于內(nèi)部的處理或優(yōu)化。例如,JIT編譯器需要內(nèi)存來存儲(chǔ)從Java虛擬機(jī)代碼轉(zhuǎn)換的本機(jī)代碼(儲(chǔ)存在CodeCache中),以獲得高性能。

從 OpenJDK8 起有了一個(gè)很 nice 的虛擬機(jī)內(nèi)部功能:Native Memory Tracking (NMT) 。我們可以使用 NMT 來追蹤了解 JVM 的內(nèi)存使用詳情(即上圖中的 JVM Memory 部分),幫助我們排查內(nèi)存增長(zhǎng)與內(nèi)存泄漏相關(guān)的問題。

2.如何使用

2.1 開啟 NMT

默認(rèn)情況下,NMT是處于關(guān)閉狀態(tài)的,我們可以通過設(shè)置 JVM 啟動(dòng)參數(shù)來開啟:-XX:NativeMemoryTracking=[off | summary | detail]。

注意:啟用NMT會(huì)導(dǎo)致5% -10%的性能開銷。

NMT 使用選項(xiàng)如下表所示:

NMT 選項(xiàng) 說明
off 不跟蹤 JVM 本地內(nèi)存使用情況。如果不指定 -XX:NativeMemoryTracking 選項(xiàng)則默認(rèn)為off。
summary 僅跟蹤 JVM 子系統(tǒng)(如:Java heap、class、code、thread等)的內(nèi)存使用情況。
detail 除了通過 JVM 子系統(tǒng)跟蹤內(nèi)存使用情況外,還可以通過單獨(dú)的 CallSite、單獨(dú)的虛擬內(nèi)存區(qū)域及其提交區(qū)域來跟蹤內(nèi)存使用情況。

我們注意到,如果想使用 NMT 觀察 JVM 的內(nèi)存使用情況,我們必須重啟 JVM 來設(shè)置 XX:NativeMemoryTracking 的相關(guān)選項(xiàng),但是重啟會(huì)使得我們丟失想要查看的現(xiàn)場(chǎng),只能等到問題復(fù)現(xiàn)時(shí)才能繼續(xù)觀察。

筆者試圖通過一種不用重啟 JVM 的方式來開啟 NMT ,但是很遺憾目前沒有這樣的功能。

JVM 啟動(dòng)后只有被標(biāo)記為 manageable 的參數(shù)才可以動(dòng)態(tài)修改或者說賦值,我們可以通過 JDK management interface (com.sun.management.HotSpotDiagnosticMXBean API) 或者 jinfo -flag 命令來進(jìn)行動(dòng)態(tài)修改的操作,讓我們看下所有可以被修改的參數(shù)值(JDK8):

java-XX:+PrintFlagsFinal|grepmanageable

intxCMSAbortablePrecleanWaitMillis=100{manageable}
intxCMSTriggerInterval=-1{manageable}
intxCMSWaitDuration=2000{manageable}
boolHeapDumpAfterFullGC=false{manageable}
boolHeapDumpBeforeFullGC=false{manageable}
boolHeapDumpOnOutOfMemoryError=false{manageable}
ccstrHeapDumpPath={manageable}
uintxMaxHeapFreeRatio=100{manageable}
uintxMinHeapFreeRatio=0{manageable}
boolPrintClassHistogram=false{manageable}
boolPrintClassHistogramAfterFullGC=false{manageable}
boolPrintClassHistogramBeforeFullGC=false{manageable}
boolPrintConcurrentLocks=false{manageable}
boolPrintGC=false{manageable}
boolPrintGCDateStamps=false{manageable}
boolPrintGCDetails=false{manageable}
boolPrintGCID=false{manageable}
boolPrintGCTimeStamps=false{manageable}

很顯然,其中不包含 NativeMemoryTracking 。

2.2 使用 jcmd 訪問 NMT 數(shù)據(jù)

我們可以通過jcmd命令來很方便的查看 NMT 相關(guān)的數(shù)據(jù):

jcmdVM.native_memory[summary|detail|baseline|summary.diff|detail.diff|shutdown][scale=KB|MB|GB]

jcmd 操作 NMT 選項(xiàng)如下表所示:

jcmd NMT 選項(xiàng) 說明
summary 打印按類別匯總的摘要信息
detail 打印按類別匯總的內(nèi)存使用情況
打印虛擬內(nèi)存映射
打印按 call site 匯總的內(nèi)存使用情況
baseline 創(chuàng)建一個(gè)新的內(nèi)存使用狀況的快照,用以進(jìn)行比較
summary.diff 根據(jù)上一個(gè) baseline 基線打印新的 summary 對(duì)比報(bào)告
detail.diff 根據(jù)上一個(gè) baseline 基線打印新的 detail 對(duì)比報(bào)告
shutdown 停止NMT

NMT 默認(rèn)打印的報(bào)告是 KB 來進(jìn)行呈現(xiàn)的,為了滿足我們不同的需求,我們可以使用scale=MB | GB來更加直觀的打印數(shù)據(jù)。

創(chuàng)建 baseline 之后使用 diff 功能可以很直觀地對(duì)比出兩次 NMT 數(shù)據(jù)之間的差距。

看到 shutdown 選項(xiàng),筆者本能的一激靈,既然我們可以通過 shutdown 來關(guān)閉 NMT ,那為什么不能通過逆向 shutdown 功能來動(dòng)態(tài)的開啟 NMT 呢?筆者找到 shutdown 相關(guān)源碼(以下都是基于 OpenJDK 8):

#hotspot/src/share/vm/services/nmtDCmd.cpp
voidNMTDCmd::execute(DCmdSourcesource,TRAPS){

//CheckNMTstate
//nativememorytrackinghastobeon
if(MemTracker::tracking_level()==NMT_off){
output()->print_cr("Nativememorytrackingisnotenabled");
return;
}elseif(MemTracker::tracking_level()==NMT_minimal){
output()->print_cr("Nativememorytrackinghasbeenshutdown");
return;
}

......
//執(zhí)行shutdown操作
elseif(_shutdown.value()){
MemTracker::shutdown();
output()->print_cr("Nativememorytrackinghasbeenturnedoff");
}
......

}

#hotspot/src/share/vm/services/memTracker.cpp
//ShutdowncanonlybeissuedviaJCmd,andNMTJCmdisserializedbylock
voidMemTracker::shutdown(){
//WecanonlyshutdownNMTtominimaltrackinglevelifitiseveron.
if(tracking_level()>NMT_minimal){
transition_to(NMT_minimal);
}
}

#hotspot/src/share/vm/services/nmtCommon.hpp
//Nativememorytrackinglevel//NMT的追蹤等級(jí)
enumNMT_TrackingLevel{
NMT_unknown=0xFF,
NMT_off=0x00,
NMT_minimal=0x01,
NMT_summary=0x02,
NMT_detail=0x03
};

遺憾的是通過源碼我們發(fā)現(xiàn),shutdown 操作只是將 NMT 的追蹤等級(jí) tracking_level 變成了 NMT_minimal 狀態(tài)(而并不是直接變成了 off 狀態(tài)),注意注釋:We can only shutdown NMT to minimal tracking level if it is ever on(即我們只能將NMT關(guān)閉到最低跟蹤級(jí)別,如果它曾經(jīng)打開)。

這就導(dǎo)致了如果我們沒有開啟過 NMT ,那就沒辦法通過魔改 shutdown 操作逆向打開 NMT ,因?yàn)?NMT 追蹤的部分內(nèi)存只在 JVM 啟動(dòng)初始化的階段進(jìn)行記錄(如在初始化堆內(nèi)存分配的過程中通過 NMT_TrackingLevel level = MemTracker::tracking_level(); 來獲取 NMT 的追蹤等級(jí),視等級(jí)來記錄內(nèi)存使用情況),JVM 啟動(dòng)之后再開啟 NMT 這部分內(nèi)存的使用情況就無法記錄,所以目前來看,還是只能在重啟 JVM 后開啟 NMT。

至于提供 shutdown 功能的原因,應(yīng)該就是讓用戶在開啟 NMT 功能之后如果想要關(guān)閉,不用再次重啟 JVM 進(jìn)程。shutdown 會(huì)清理虛擬內(nèi)存用來追蹤的數(shù)據(jù)結(jié)構(gòu),并停止一些追蹤的操作(如記錄 malloc 內(nèi)存的分配)來降低開啟 NMT 帶來的性能耗損,并且通過源碼可以發(fā)現(xiàn) tracking_level 變成 NMT_minimal 狀態(tài)后也不會(huì)再執(zhí)行 jcmd VM.native_memory 命令相關(guān)的操作。

2.3 虛擬機(jī)退出時(shí)獲取 NMT 數(shù)據(jù)

除了在虛擬機(jī)運(yùn)行時(shí)獲取 NMT 數(shù)據(jù),我們還可以通過兩個(gè)參數(shù):-XX:+UnlockDiagnosticVMOptions和-XX:+PrintNMTStatistics,來獲取虛擬機(jī)退出時(shí)內(nèi)存使用情況的數(shù)據(jù)(輸出數(shù)據(jù)的詳細(xì)程度取決于你設(shè)定的跟蹤級(jí)別,如 summary/detail 等)。

-XX:+UnlockDiagnosticVMOptions:解鎖用于診斷 JVM 的選項(xiàng),默認(rèn)關(guān)閉。
-XX:+PrintNMTStatistics:當(dāng)啟用 NMT 時(shí),在虛擬機(jī)退出時(shí)打印內(nèi)存使用情況,默認(rèn)關(guān)閉,需要開啟前置參數(shù) -XX:+UnlockDiagnosticVMOptions 才能正常使用。

3.NMT 內(nèi)存 & OS 內(nèi)存概念差異性

我們可以做一個(gè)簡(jiǎn)單的測(cè)試,使用如下參數(shù)啟動(dòng) JVM :

-Xmx1G-Xms1G-XX:+UseG1GC-XX:MaxMetaspaceSize=256m-XX:MaxDirectMemorySize=256m-XX:ReservedCodeCacheSize=256M-XX:NativeMemoryTracking=detail

然后使用 NMT 查看內(nèi)存使用情況(因各環(huán)境資源參數(shù)不一樣,部分未明確設(shè)置數(shù)據(jù)可能由虛擬機(jī)根據(jù)資源自行計(jì)算得出,以下數(shù)據(jù)僅供參考):

jcmdVM.native_memorydetail

NMT 會(huì)輸出如下日志:

NativeMemoryTracking:

Total:reserved=2813709KB,committed=1497485KB
-JavaHeap(reserved=1048576KB,committed=1048576KB)
(mmap:reserved=1048576KB,committed=1048576KB)

-Class(reserved=1056899KB,committed=4995KB)
(classes#442)
(malloc=131KB#259)
(mmap:reserved=1056768KB,committed=4864KB)

-Thread(reserved=258568KB,committed=258568KB)
(thread#127)
(stack:reserved=258048KB,committed=258048KB)
(malloc=390KB#711)
(arena=130KB#234)

-Code(reserved=266273KB,committed=4001KB)
(malloc=33KB#309)
(mmap:reserved=266240KB,committed=3968KB)

-GC(reserved=164403KB,committed=164403KB)
(malloc=92723KB#6540)
(mmap:reserved=71680KB,committed=71680KB)

-Compiler(reserved=152KB,committed=152KB)
(malloc=4KB#36)
(arena=148KB#21)

-Internal(reserved=14859KB,committed=14859KB)
(malloc=14827KB#3632)
(mmap:reserved=32KB,committed=32KB)

-Symbol(reserved=1423KB,committed=1423KB)
(malloc=936KB#111)
(arena=488KB#1)

-NativeMemoryTracking(reserved=330KB,committed=330KB)
(malloc=118KB#1641)
(trackingoverhead=211KB)

-ArenaChunk(reserved=178KB,committed=178KB)
(malloc=178KB)

-Unknown(reserved=2048KB,committed=0KB)
(mmap:reserved=2048KB,committed=0KB)

......

大家可能會(huì)發(fā)現(xiàn) NMT 所追蹤的內(nèi)存(即 JVM 中的 Reserved、Committed)與操作系統(tǒng) OS (此處指Linux)的內(nèi)存概念存在一定的差異性。

首先按我們理解的操作系統(tǒng)的概念:

操作系統(tǒng)對(duì)內(nèi)存的分配管理典型地分為兩個(gè)階段:保留(reserve)和提交(commit)。保留階段告知系統(tǒng)從某一地址開始到后面的dwSize大小的連續(xù)虛擬內(nèi)存需要供程序使用,進(jìn)程其他分配內(nèi)存的操作不得使用這段內(nèi)存;提交階段將虛擬地址映射到對(duì)應(yīng)的真實(shí)物理內(nèi)存中,這樣這塊內(nèi)存就可以正常使用[1]。

如果使用 top 或者 smem 等命令查看剛才啟動(dòng)的 JVM 進(jìn)程會(huì)發(fā)現(xiàn):

top

PIDUSERPRNIVIRTRESSHRS%CPU%MEMTIME+COMMAND
36257dou+20010.8g5420017668S99.70.013:04.15java

此時(shí)疑問就產(chǎn)生了,為什么 NMT 中的 committed ,即日志詳情中 Total: reserved=2813709KB, committed=1497485KB 中的 1497485KB 與 top 中 RES 的大小54200KB 存在如此大的差異?

使用 man 查看 top 中 RES 的概念(不同版本 Linux 可能不同):

RES--ResidentMemorySize(KiB)
Asubsetofthevirtualaddressspace(VIRT)representingthenon-swappedphysicalmemoryataskiscurrentlyusing.ItisalsothesumoftheRSan,
RSfdandRSshfields.

Itcanincludeprivateanonymouspages,privatepagesmappedtofiles(includingprogramimagesandsharedlibraries)plussharedanonymouspages.
AllsuchmemoryisbackedbytheswapfilerepresentedseparatelyunderSWAP.

Lastly,thisfieldmayalsoincludesharedfile-backedpageswhich,whenmodified,actasadedicatedswapfileandthuswillneverimpactSWAP.

RES 表示任務(wù)當(dāng)前使用的非交換物理內(nèi)存(此時(shí)未發(fā)生swap),那按對(duì)操作系統(tǒng) commit 提交內(nèi)存的理解,這兩者貌似應(yīng)該對(duì)上,為何現(xiàn)在差距那么大呢?

筆者一開始猜測(cè)是 JVM 的 uncommit 機(jī)制(如 JEP 346[2],支持 G1 在空閑時(shí)自動(dòng)將 Java 堆內(nèi)存返回給操作系統(tǒng),BiSheng JDK 對(duì)此做了增強(qiáng)與改進(jìn)[3])造成的,JVM 在 uncommit 將內(nèi)存返還給 OS 之后,NMT 沒有除去返還的內(nèi)存導(dǎo)致統(tǒng)計(jì)錯(cuò)誤。

但是在翻閱了源碼之后發(fā)現(xiàn),G1 在 shrink 縮容的時(shí)候,通常調(diào)用鏈路如下:

G1CollectedHeap::shrink->
G1CollectedHeap::shrink_helper->
HeapRegionManager::shrink_by->
HeapRegionManager::uncommit_regions->
G1PageBasedVirtualSpace::uncommit->
G1PageBasedVirtualSpace::uncommit_internal->
os::uncommit_memory

忽略細(xì)節(jié),uncommit 會(huì)在最后調(diào)用 os::uncommit_memory ,查看 os::uncommit_memory 源碼:

boolos::uncommit_memory(char*addr,size_tbytes){
boolres;
if(MemTracker::tracking_level()>NMT_minimal){
Trackertkr=MemTracker::get_virtual_memory_uncommit_tracker();
res=pd_uncommit_memory(addr,bytes);
if(res){
tkr.record((address)addr,bytes);
}
}else{
res=pd_uncommit_memory(addr,bytes);
}
returnres;
}

可以發(fā)現(xiàn)在返還 OS 內(nèi)存之后,MemTracker 是進(jìn)行了統(tǒng)計(jì)的,所以此處的誤差不是由 uncommit 機(jī)制造成的。

既然如此,那又是由什么原因造成的呢?筆者在追蹤 JVM 的內(nèi)存分配邏輯時(shí)發(fā)現(xiàn)了一些端倪,此處以Code Cache(存放 JVM 生成的 native code、JIT編譯、JNI 等都會(huì)編譯代碼到 native code,其中 JIT 生成的 native code 占用了 Code Cache 的絕大部分空間)的初始化分配為例,其大致調(diào)用鏈路為下:

InitializeJVM->
Thread::vreate_vm->
init_globals->
codeCache_init->
CodeCache::initialize->
CodeHeap::reserve->
VirtualSpace::initialize->
VirtualSpace::initialize_with_granularity->
VirtualSpace::expand_by->
os::commit_memory

查看 os::commit_memory 相關(guān)源碼:

boolos::commit_memory(char*addr,size_tsize,size_talignment_hint,
boolexecutable){
boolres=os::pd_commit_memory(addr,size,alignment_hint,executable);
if(res){
MemTracker::record_virtual_memory_commit((address)addr,size,CALLER_PC);
}
returnres;
}

我們發(fā)現(xiàn) MemTracker 在此記錄了 commit 的內(nèi)存供 NMT 用以統(tǒng)計(jì)計(jì)算,繼續(xù)查看 os::pd_commit_memory 源碼,可以發(fā)現(xiàn)其調(diào)用了 os::commit_memory_impl 函數(shù)。

查看 os::commit_memory_impl 源碼:

intos::commit_memory_impl(char*addr,size_tsize,boolexec){
intprot=exec?PROT_READ|PROT_WRITE|PROT_EXEC:PROT_READ|PROT_WRITE;
uintptr_tres=(uintptr_t)::mmap(addr,size,prot,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,-1,0);
if(res!=(uintptr_t)MAP_FAILED){
if(UseNUMAInterleaving){
numa_make_global(addr,size);
}
return0;
}

interr=errno;//saveerrnofrommmap()callabove

if(!recoverable_mmap_error(err)){
warn_fail_commit_memory(addr,size,exec,err);
vm_exit_out_of_memory(size,OOM_MMAP_ERROR,"committingreservedmemory.");
}

returnerr;
}

問題的原因就在 uintptr_t res = (uintptr_t) ::mmap(addr, size, prot, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); 這段代碼上。

我們發(fā)現(xiàn),此時(shí)申請(qǐng)內(nèi)存執(zhí)行的是 mmap 函數(shù),并且傳遞的 port 參數(shù)是 PROT_READ|PROT_WRITE|PROT_EXEC 或 PROT_READ|PROT_WRITE ,使用 man 查看 mmap ,其中相關(guān)描述為:

Theprotargumentdescribesthedesiredmemoryprotectionofthemapping(andmustnotconflictwiththeopenmodeofthefile).ItiseitherPROT_NONE
orthebitwiseORofoneormoreofthefollowingflags:

PROT_EXECPagesmaybeexecuted.

PROT_READPagesmayberead.

PROT_WRITEPagesmaybewritten.

PROT_NONEPagesmaynotbeaccessed.

由此我們可以看出,JVM 中所謂的 commit 內(nèi)存,只是將內(nèi)存 mmaped 映射為可讀可寫可執(zhí)行的狀態(tài)!而在 Linux 中,在分配內(nèi)存時(shí)又是 lazy allocation 的機(jī)制,只有在進(jìn)程真正訪問時(shí)才分配真實(shí)的物理內(nèi)存。所以 NMT 中所統(tǒng)計(jì)的 committed 并不是對(duì)應(yīng)的真實(shí)的物理內(nèi)存,自然與 RES 等統(tǒng)計(jì)方式無法對(duì)應(yīng)起來。

所以 JVM 為我們提供了一個(gè)參數(shù) -XX:+AlwaysPreTouch,使我們可以在啟動(dòng)之初就按照內(nèi)存頁粒度都訪問一遍 Heap,強(qiáng)制為其分配物理內(nèi)存以減少運(yùn)行時(shí)再分配內(nèi)存造成的延遲(但是相應(yīng)的會(huì)影響 JVM 進(jìn)程初始化啟動(dòng)的時(shí)間),查看相關(guān)代碼:

voidos::pretouch_memory(char*start,char*end){
for(volatilechar*p=start;p

讓我們來驗(yàn)證下,開啟 -XX:+AlwaysPreTouch 前后的效果。

NMT 的 heap 地址范圍:

Virtualmemorymap:
[0x00000000c0000000-0x0000000100000000]reserved1048576KBforJavaHeapfrom
[0x0000ffff93ea36d8]ReservedHeapSpace::ReservedHeapSpace(unsignedlong,unsignedlong,bool,char*)+0xb8
[0x0000ffff93e67f68]Universe::reserve_heap(unsignedlong,unsignedlong)+0x2d0
[0x0000ffff93898f28]G1CollectedHeap::initialize()+0x188
[0x0000ffff93e68594]Universe::initialize_heap()+0x15c

[0x00000000c0000000-0x0000000100000000]committed1048576KBfrom
[0x0000ffff938bbe8c]G1PageBasedVirtualSpace::commit_internal(unsignedlong,unsignedlong)+0x14c
[0x0000ffff938bc08c]G1PageBasedVirtualSpace::commit(unsignedlong,unsignedlong)+0x11c
[0x0000ffff938bf774]G1RegionsLargerThanCommitSizeMapper::commit_regions(unsignedint,unsignedlong)+0x5c
[0x0000ffff93943f54]HeapRegionManager::commit_regions(unsignedint,unsignedlong)+0x7c

對(duì)應(yīng)該地址的/proc/{pid}/smaps:

//開啟前//開啟后
c0000000-100080000rw-p0000000000:000c0000000-100080000rw-p0000000000:000
Size:1049088kB Size:1049088kB
KernelPageSize:4kBKernelPageSize: 4kB
MMUPageSize: 4kBMMUPageSize: 4kB
Rss: 792kBRss: 1049088kB
Pss:792kBPss: 1049088kB
Shared_Clean: 0kBShared_Clean: 0kB
Shared_Dirty: 0kBShared_Dirty: 0kB
Private_Clean: 0kBPrivate_Clean: 0kB
Private_Dirty: 792kBPrivate_Dirty: 1049088kB
Referenced: 792kBReferenced: 1048520kB
Anonymous: 792kBAnonymous: 1049088kB
LazyFree: 0kBLazyFree: 0kB
AnonHugePages: 0kBAnonHugePages: 0kB
ShmemPmdMapped: 0kBShmemPmdMapped: 0kB
Shared_Hugetlb:0kBShared_Hugetlb: 0kB
Private_Hugetlb:0kBPrivate_Hugetlb: 0kB
Swap:0kBSwap: 0kB
SwapPss:0kBSwapPss: 0kB
Locked:0kBLocked: 0kB
VmFlags:rdwrmrmwmeacVmFlags:rdwrmrmwmeac

對(duì)應(yīng)的/proc/{pid}/status:

//開啟前//開啟后
......
VmHWM:54136kBVmHWM:1179476kB
VmRSS:54136kBVmRSS:1179476kB
......
VmSwap:0kBVmSwap:0kB
...

開啟參數(shù)后的 top:

PIDUSERPRNIVIRTRESSHRS%CPU%MEMTIME+COMMAND
85376dou+20010.8g1.1g17784S99.70.414:56.31java

觀察對(duì)比我們可以發(fā)現(xiàn),開啟 AlwaysPreTouch 參數(shù)后,NMT 統(tǒng)計(jì)的 commited 已經(jīng)與 top 中的 RES 差不多了,之所以不完全相同是因?yàn)樵搮?shù)只能 Pre-touch 分配 Java heap 的物理內(nèi)存,至于其他的非 heap 的內(nèi)存,還是受到 lazy allocation 機(jī)制的影響。

同理我們可以簡(jiǎn)單看下 JVM 的 reserve 機(jī)制:

#hotspot/src/share/vm/runtime/os.cpp
char*os::reserve_memory(size_tbytes,char*addr,size_talignment_hint,
MEMFLAGSflags){
char*result=pd_reserve_memory(bytes,addr,alignment_hint);
if(result!=NULL){
MemTracker::record_virtual_memory_reserve((address)result,bytes,CALLER_PC);
MemTracker::record_virtual_memory_type((address)result,flags);
}

returnresult;
}

#hotspot/src/os/linux/vm/os_linux.cpp
char*os::pd_reserve_memory(size_tbytes,char*requested_addr,
size_talignment_hint){
returnanon_mmap(requested_addr,bytes,(requested_addr!=NULL));
}

staticchar*anon_mmap(char*requested_addr,size_tbytes,boolfixed){
......
addr=(char*)::mmap(requested_addr,bytes,PROT_NONE,
flags,-1,0);
......
}

reserve 通過mmap(requested_addr, bytes, PROT_NONE, flags, -1, 0);來將內(nèi)存映射為 PROT_NONE,這樣其他的 mmap/malloc 等就不能調(diào)用使用,從而達(dá)到了 guard memory 或者說 guard pages 的目的。

OpenJDK 社區(qū)其實(shí)也注意到了 NMT 內(nèi)存與 OS 內(nèi)存差異性的問題,所以社區(qū)也提出了相應(yīng)的 Enhancement 來增強(qiáng)功能:

JDK-8249666[4]:

目前 NMT 將分配的內(nèi)存顯示為 Reserved 或 Committed。而在 top 或 pmap 的輸出中,首次使用(即 touch)之前 Reserved 和 Committed 的內(nèi)存都將顯示為 Virtual memory。只有在內(nèi)存頁(通常是4k)首次寫入后,它才會(huì)消耗物理內(nèi)存,并出現(xiàn)在 top/pmap 輸出的 “常駐內(nèi)存”(即 RSS)中。

當(dāng)前NMT輸出的主要問題是,它無法區(qū)分已 touch 和未 touch 的 Committed 內(nèi)存。

該 Enhancement 提出可以使用 mincore()[5]來查找 NMT 的 Committed 中 RSS 的部分,mincore() 系統(tǒng)調(diào)用讓一個(gè)進(jìn)程能夠確定一塊虛擬內(nèi)存區(qū)域中的分頁是否駐留在物理內(nèi)存中。mincore()已在JDK-8191369 NMT:增強(qiáng)線程堆棧跟蹤中實(shí)現(xiàn),需要將其擴(kuò)展到所有其他類型的內(nèi)存中(如 Java 堆)。

遺憾的是該 Enhancement 至今仍是 Unresolved 狀態(tài)。

JDK-8191369[6]:

1 中提到的 NMT:增強(qiáng)線程堆棧跟蹤。使用 mincore() 來追蹤駐留在物理內(nèi)存中的線程堆棧的大小,用以解決線程堆棧追蹤時(shí)有時(shí)會(huì)夸大內(nèi)存使用情況的痛點(diǎn)。

該 Enhancement 已經(jīng)在 JDK11 中實(shí)現(xiàn)。

由于內(nèi)容較多,關(guān)于NMT追蹤區(qū)域分析的內(nèi)容將在下篇文章進(jìn)行分享,敬請(qǐng)期待!

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 內(nèi)存
    +關(guān)注

    關(guān)注

    8

    文章

    2976

    瀏覽量

    73815
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2952

    瀏覽量

    104489
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4284

    瀏覽量

    62325
  • 虛擬機(jī)
    +關(guān)注

    關(guān)注

    1

    文章

    905

    瀏覽量

    28021

原文標(biāo)題:Native Memory Tracking 詳解(1):基礎(chǔ)介紹

文章出處:【微信號(hào):openEulercommunity,微信公眾號(hào):openEuler】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Java多線程的用法

    本文將介紹一下Java多線程的用法。 基礎(chǔ)介紹 什么是多線程 指的是在一個(gè)進(jìn)程中同時(shí)運(yùn)行多個(gè)線程,每個(gè)線程都可以獨(dú)立執(zhí)行不同的任務(wù)或操作。 與單線程相比,多線程可以提高程序的并發(fā)性和響
    的頭像 發(fā)表于 09-30 17:07 ?916次閱讀

    Linux 查看進(jìn)程和刪除進(jìn)程

    ps 命令用于查看當(dāng)前正在運(yùn)行的進(jìn)程。grep 是搜索例如: ps -ef | grep java表示查看所有進(jìn)程里 CMD 是 java進(jìn)程
    發(fā)表于 04-24 00:04

    Linux上對(duì)進(jìn)程進(jìn)行內(nèi)存分析和內(nèi)存泄漏定位

    在Linux產(chǎn)品開發(fā)過程中,通常需要注意系統(tǒng)內(nèi)存使用量,和評(píng)估單一進(jìn)程內(nèi)存使用情況,便于我們選取合適的機(jī)器配置,來部署我們的產(chǎn)品。Linux本身提供了一些工具方便我們達(dá)成這些需求,查
    發(fā)表于 07-09 08:15

    linux內(nèi)存進(jìn)程查看

    用 'top -i' 看看有多少進(jìn)程處于 Running 狀態(tài),可能系統(tǒng)存在內(nèi)存或 I/O 瓶頸,用 free 看看系統(tǒng)內(nèi)存使用情況,swap 是否被占用很多,用 iostat 看看
    發(fā)表于 07-16 06:28

    Java程序內(nèi)存低效使用問題的分析

    Java程序內(nèi)存的低效使用是導(dǎo)致其性能問題的主要因素。該文分析了泄漏對(duì)象、蚍蜉對(duì)象和空閑對(duì)象3類導(dǎo)致內(nèi)存低效使用的情況,探討解決上述問題的方法,并提出構(gòu)造對(duì)象行為模式
    發(fā)表于 04-09 09:39 ?12次下載

    java線程內(nèi)存模型

    一、Java內(nèi)存模型 按照官方的說法:Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配。 JVM主要管理兩種類型內(nèi)
    發(fā)表于 09-27 10:55 ?0次下載
    <b class='flag-5'>java</b>線程<b class='flag-5'>內(nèi)存</b>模型

    Java內(nèi)存模型及原理分析

    一、Java內(nèi)存模型 按照官方的說法:Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配。 JVM主要管理兩種類型內(nèi)
    發(fā)表于 09-28 11:49 ?0次下載
    <b class='flag-5'>Java</b><b class='flag-5'>內(nèi)存</b>模型及原理分析

    Linux下進(jìn)程內(nèi)存結(jié)構(gòu)

    Linux操作系統(tǒng)采用虛擬內(nèi)存管理技術(shù),使得每個(gè)進(jìn)程都有各自互不干涉的進(jìn)程地址空間。該地址空間是大小為4GB的線性虛擬空間,用戶看到和接觸到的都是該虛擬地址,無法看到實(shí)際的物理
    發(fā)表于 06-01 09:17 ?1463次閱讀
    Linux下<b class='flag-5'>進(jìn)程</b>的<b class='flag-5'>內(nèi)存</b>結(jié)構(gòu)

    進(jìn)程虛擬內(nèi)存布局以及進(jìn)程的虛擬內(nèi)存分配釋放流程,涉及的代碼

    我們計(jì)劃通過一系列文章來介紹虛擬內(nèi)存分配/釋放,缺頁處理,內(nèi)存壓縮/回收,內(nèi)存分配器等知識(shí),梳理虛擬內(nèi)存的管理。本章節(jié)結(jié)合代碼
    的頭像 發(fā)表于 06-28 09:38 ?4012次閱讀

    java內(nèi)存溢出的幾種原因和解決辦法

    Java是一種使用垃圾回收機(jī)制的編程語言,由于自動(dòng)內(nèi)存管理機(jī)制的存在,Java程序中發(fā)生內(nèi)存溢出(Out of Memory)錯(cuò)誤的情況相對(duì)
    的頭像 發(fā)表于 11-23 14:44 ?5959次閱讀

    java內(nèi)存溢出排查方法

    過程中常見的問題之一,可能導(dǎo)致應(yīng)用程序崩潰、性能下降甚至系統(tǒng)崩潰。在本文中,將詳細(xì)介紹如何排查和解決Java內(nèi)存溢出問題。 一、什么是Java內(nèi)存
    的頭像 發(fā)表于 11-23 14:46 ?3036次閱讀

    如何查看java程序的內(nèi)存分布

    。 程序計(jì)數(shù)器: 程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用是指示當(dāng)前線程執(zhí)行的字節(jié)碼指令的行號(hào)。在多線程環(huán)境下,每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,用于記錄當(dāng)前線程執(zhí)行的字節(jié)碼指令。 Java虛擬機(jī)棧:
    的頭像 發(fā)表于 11-23 14:47 ?987次閱讀

    jmap dump內(nèi)存的命令是

    jmap dump是Java內(nèi)存映像工具(Java Memory Map Tool)的一個(gè)功能,用于生成Java虛擬機(jī)(JVM)中的堆內(nèi)存
    的頭像 發(fā)表于 12-05 10:38 ?3027次閱讀

    jvm內(nèi)存分析命令和工具

    介紹JVM內(nèi)存分析命令和工具,并詳細(xì)介紹它們的使用方法和功能。 一、JVM內(nèi)存分析命令 jps命令:jps命令用于顯示當(dāng)前系統(tǒng)中正在運(yùn)行的Java
    的頭像 發(fā)表于 12-05 11:07 ?1112次閱讀

    java虛擬機(jī)內(nèi)存包括遠(yuǎn)空間內(nèi)存

    Java虛擬機(jī)(JVM)內(nèi)存Java程序執(zhí)行時(shí)使用的內(nèi)存空間的總稱,包括了Java堆、方法區(qū)
    的頭像 發(fā)表于 12-05 14:15 ?366次閱讀