面试官上来就问:Java 进程中有哪些组件会占用内存?
本文的內(nèi)容來(lái)自 StackOverflow 的一個(gè)問答:Java using much more memory than heap size (or size correctly Docker memory limit)
有網(wǎng)友留言,今天去參加面試,面試官上來(lái)就問:你能解釋為什么 Java 進(jìn)程占用內(nèi)存遠(yuǎn)超過(guò)堆內(nèi)存大小?如何正確計(jì)算 Docker 內(nèi)存限制?有沒有辦法減少 Java 進(jìn)程的堆外內(nèi)存(off-heap memeory)占用?
面對(duì)這類問題,這位網(wǎng)友是這樣答復(fù)的:
Java 進(jìn)程使用的虛擬內(nèi)存遠(yuǎn)遠(yuǎn)超過(guò) Java 堆大小。要知道 JVM 包括許多子系統(tǒng),垃圾回收器、類裝載器、JIT 編譯器等等。所有這些子系統(tǒng)運(yùn)行都需要占用內(nèi)存。JVM 不是內(nèi)存唯一的消費(fèi)者,Java Class Library 在內(nèi)的所有 Native Library 也會(huì)占用內(nèi)存。對(duì)于內(nèi)存跟蹤工具來(lái)說(shuō)這些開銷甚至無(wú)法跟蹤。Java 應(yīng)用程序本身還可以通過(guò)直接?ByteBuffers?使用堆外內(nèi)存。
這塊知識(shí)點(diǎn)其實(shí)需要包含很多個(gè)點(diǎn),當(dāng)突如其來(lái)一個(gè)這類問題的時(shí)候,我們很難回答的很全面。在這里我們先系統(tǒng)的總結(jié)一下,如有遺留,請(qǐng)?jiān)谖哪┝粞浴?/p>
1. 究竟 Java 進(jìn)程中有哪些組件會(huì)占用內(nèi)存?
通過(guò) Native Memory Tracking 可以觀察到有以下 JVM 組件。
1.1 Java 堆
最顯而易見的就是 Java 堆,它是 Java 對(duì)象存在的地方。它會(huì)占用?-Xmx?參數(shù)指定大小的內(nèi)存。
1.2 垃圾回收器
GC 需要額外的內(nèi)存進(jìn)行堆管理,主要用于 GC 自身的結(jié)構(gòu)與算法。這些結(jié)構(gòu)包括 Mark Bitmap、Mark Stack(遍歷對(duì)象關(guān)系圖)、Remembered Set(記錄 region 之間引用)等等。其中一些可以直接調(diào)優(yōu),例如?-XX: MarkStackSizeMax?選項(xiàng),另一些依賴于堆布局。其中 G1 region (-XX:G1HeapRegionSize)占用內(nèi)存較大,Remembered Set 占用內(nèi)存較小。
GC 的內(nèi)存開銷因算法而異,其中?-XX:+UseSerialGC?與?-XX:+UseShenandoahGC?的開銷最小,而 G1 或 CMS 則會(huì)輕松占用大約10%的堆內(nèi)存。
1.3 代碼緩存
代碼緩存包含動(dòng)態(tài)生成的代碼,JIT 編譯生成的方法、解釋器以及運(yùn)行時(shí) stub 代碼。代碼大小受?-XX:ReservedCodeCacheSize?選項(xiàng)限制(默認(rèn)為240M)。關(guān)閉?-XX:-TieredCompilation?可以減少已編譯代碼的數(shù)量,從而減小代碼緩存。
1.4 編譯器
JIT 編譯器本身工作時(shí)也需要內(nèi)存。可以通過(guò)關(guān)閉 Tiered Compilation 或者?-XX:CICompilerCount?減少編譯使用的線程數(shù)。
1.5 類加載
類的元數(shù)據(jù)存儲(chǔ)在 Metaspace 堆外區(qū)域中,包括方法字節(jié)碼、符號(hào)、常量池、注解等。加載的類越多,使用的元數(shù)據(jù)就越多。可以通過(guò)?-XX:MaxMetaspaceSize(默認(rèn)無(wú)上限)和?-XX:CompressedClassSpaceSize(默認(rèn)1G)選項(xiàng)控制元數(shù)據(jù)總大小。
1.6 符號(hào)表
JVM 有兩個(gè)主要的 hashtable:符號(hào)表包含名稱、簽名、標(biāo)識(shí)符等,String 表包含對(duì) interned String 引用。如果 Native Memory Tracking 顯示 String 表使用了大量?jī)?nèi)存,這可能意味著應(yīng)用程序調(diào)用 String.intern 過(guò)于頻繁。
1.7 線程
線程堆棧也會(huì)申請(qǐng)內(nèi)存。堆棧大小由?-Xss?選項(xiàng)指定,默認(rèn)每個(gè)線程1M,幸運(yùn)的是情況并非那么糟糕。操作系統(tǒng)會(huì)以延遲分配的方式分配內(nèi)存頁(yè)面,比如在第一次使用時(shí)分配,因此實(shí)際使用的內(nèi)存要低得多,通常每個(gè)線程堆棧占用80至200KB。我編寫了一個(gè)腳本評(píng)估有多少 RSS 屬于 Java 線程堆棧。
還有其他 JVM 部件會(huì)占用本地內(nèi)存,但它們?cè)诳們?nèi)存消耗中通常比例不大。
2. Direct Buffer
應(yīng)用程序可以通過(guò) ByteBuffer.allocateDirect 調(diào)用直接請(qǐng)求非堆內(nèi)存。默認(rèn)的非堆內(nèi)存大小限制由?-Xmx?選項(xiàng)指定,但也可以使用?-XX:MaxDirectMemorySize?覆蓋配置。Direct ByteBuffer 包含在 Native Memory Tracking 輸出的 Other 區(qū)域,在 JDK 11 之前包含在 Internal 區(qū)域。
通過(guò) JMX 可以在 JConsole 或 Java Mission Control 中直接看到 Direct Memory 的使用量:
除了 Direct ByteBuffer,還有?MappedByteBuffer?映射到進(jìn)程虛擬內(nèi)存中的文件。雖然 Native Memory Tracking 不對(duì)它跟蹤,但是?MappedByteBuffer?也會(huì)占用物理內(nèi)存,而且沒有一種簡(jiǎn)單的方法限制它申請(qǐng)的內(nèi)存大小。可以通過(guò)查看進(jìn)程內(nèi)存映射了解實(shí)際的內(nèi)存使用情況:pmap-x <pid>。
Address???????????Kbytes????RSS????Dirty?Mode??Mapping ... 00007f2b3e557000???39592???32956???????0?r--s-?some-file-17405-Index.db 00007f2b40c01000???39600???33092???????0?r--s-?some-file-17404-Index.db^^^^^???????????????^^^^^^^^^^^^^^^^^^^^^^^^3. Native Library
System.Loadlibrary?加載的 JNI 代碼可以不受 JVM 控制分配堆外內(nèi)存,標(biāo)準(zhǔn) Java Class Library 也是如此。尤其是未關(guān)閉的 Java 資源可能造成本地內(nèi)存泄漏。典型的例子是?ZipInputStream?和?DirectoryStream。
JVMTI 代理,尤其是 jdwp 調(diào)試代理,也會(huì)造成內(nèi)存消耗過(guò)多。
這個(gè)回答描述了如何使用 async-profiler 分析本地內(nèi)存分配。
4. Allocator 問題
進(jìn)程通常通過(guò) mmap 系統(tǒng)調(diào)用直接從操作系統(tǒng)分配內(nèi)存,或者使用標(biāo)準(zhǔn)的 libc allocator —— malloc 分配本機(jī)內(nèi)存。反過(guò)來(lái),malloc 會(huì)調(diào)用 mmap 向操作系統(tǒng)申請(qǐng)大塊內(nèi)存,然后根據(jù)自己的分配算法管理內(nèi)存塊。問題在于這種算法會(huì)造成碎片化以及過(guò)度使用虛擬內(nèi)存。
jemalloc 是 libc malloc 的一個(gè)更智能的替代選項(xiàng),使用 jemalloc 占用內(nèi)存會(huì)變得更小。
5. 總結(jié)
因?yàn)橛刑嗟囊蛩匦枰紤],沒有一種可靠的方法可以用來(lái)評(píng)估一個(gè) Java 進(jìn)程所有的內(nèi)存使用量。
總內(nèi)存?=?堆?+?代碼緩存?+?Metaspace?+?符號(hào)表?+其他?JVM?結(jié)構(gòu)?+?線程堆棧?+Direct?Buffer?+?映射文件?+Native?Library?+?Malloc?開銷?+?...雖然可以通過(guò)設(shè)置 JVM 參數(shù)縮小或限制類似代碼緩存這樣的區(qū)域,但是其他許多區(qū)域根本不受 JVM 控制。
設(shè)置 Docker 限制的一種可能的方法是觀察進(jìn)程“正常”狀態(tài)下的實(shí)際內(nèi)存使用情況。有一些工具和技術(shù)可以用來(lái)研究 Java 內(nèi)存消耗問題,Native Memory Tracking、pmap、jemalloc、async-profiler。
總結(jié)
以上是生活随笔為你收集整理的面试官上来就问:Java 进程中有哪些组件会占用内存?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开发高质量软件需要更高成本吗?
- 下一篇: Java 实现 HTTP 请求的三种方式