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