歡迎光臨
每天分享高質量文章

Java 行程中有哪些組件會占用記憶體?

(給ImportNew加星標,提高Java技能)

 

編譯:唐尤華

 

本文來自 StackOverflow 的一個問答:Java using much more memory than heap size (or size correctly Docker memory limit)  

 

題主發現 Java 行程占用記憶體遠超過堆記憶體設置的大小,於是提出了下麵的問題:

 

有誰能解釋為什麼 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。我編寫了一個[腳本][1]評估有多少 RSS 屬於 Java 執行緒堆棧。

 

[1]:https://github.com/apangin/jstackmem

 

還有其他 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 `。

 

```shell
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 除錯代理,也會造成記憶體消耗過多。

 

[這個回答][2]描述瞭如何使用 [async-profiler][3] 分析本地記憶體分配。

 

[2]:https://stackoverflow.com/a/53598622/3448419

[3]:https://github.com/jvm-profiling-tools/async-profiler/

 

4. Allocator 問題

 

行程通常通過 mmap 系統呼叫直接從操作系統分配記憶體,或者使用標準的 libc allocator —— malloc 分配本機記憶體。反過來,malloc 會呼叫 mmap 向操作系統申請大塊記憶體,然後根據自己的分配演算法管理記憶體塊。問題在於這種演算法會造成碎片化以及[過度使用虛擬記憶體][4]。

 

[4]:https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

 

[jemalloc][5] 是 libc malloc 的一個更智慧的替代選項,使用 jemalloc 占用記憶體會變得更小。

 

[5]:http://jemalloc.net/

 

5. 總結

 

因為有太多的因素需要考慮,沒有一種可靠的方法可以用來評估一個 Java 行程所有的記憶體使用量。

 

```
總記憶體 = 堆 + 代碼快取 + Metaspace + 符號表 +
        其他 JVM 結構 + 執行緒堆棧 +
        Direct Buffer + 映射檔案 +
        Native Library + Malloc 開銷 + ...
```

 

雖然可以通過設置 JVM 引數縮小或限制類似代碼快取這樣的區域,但是其他許多區域根本不受 JVM 控制。

 

設置 Docker 限制的一種可能的方法是觀察行程“正常”狀態下的實際記憶體使用情況。有一些工具和技術可以用來研究 Java 記憶體消耗問題,[Native Memory Tracking][6]、[pmap][7]、[jemalloc][5]、[async-profiler][3]。

 

[6]:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html

[7]:http://man7.org/linux/man-pages/man1/pmap.1.html

    已同步到看一看
    赞(0)

    分享創造快樂