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