為什麼 Java 進程使用的 RAM 比 Heap Size 大?

為什麼 Java 進程使用的 RAM 比 Heap Size 大?| CSDN博文精選

作者 | javaadu

出品 | CSDN 博客

Java進程使用的虛擬內存確實比Java Heap要大很多。JVM包括很多子系統:垃圾收集器、類加載系統、JIT編譯器等等,這些子系統各自都需要一定數量的RAM才能正常工作。

當一個Java進程運行時,也不僅僅是JVM在消耗RAM,很多本地庫(Java類庫中引用的本地庫)可能需要分配原生內存,這些內存無法被JVM的Native Memory Tracking機制監控到。Java應用自身也可能通過DirectByteBuffers等類來使用堆外內存。

那麼,當一個Java進程運行時,有哪些部分在消耗內存呢?這裡我們只展示哪些可以被Native Memory Tracking監控到的部分。

为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

JVM部分

Java Heap:最明顯的部分,Java對象在這個區域分配和回收,Heap的最大值由-Xmx決定。

Garbage Collector:GC的數據結構和算法需要額外的內存對堆內存進行管理。這些數據結構包括:Mark Bitmap、Mark Stack(用於跟蹤存活的對象)、Remembered Sets(用於記錄region之間的引用)等等。這些數據結構中的一些是可以直接調整的,例如:-XX:MarkStackSizeMax,其他的則依賴於堆的分佈,例如:分區大小,-XX:G1HeapRegionSize,這個值越大Remembered Sets的值越小。不同的GC算法需要的額外內存是不同的,-XX: UseSerialGC和-XX: UseShenandoahGC需要較小的額外內存,G1和CMS則需要Heap size的10%作為額外內存。

Code Cache:用於存放動態生成的代碼:JIT編譯的方法、攔截器和運行時存根。這個區域的大小由-XX:ReservedCodeCacheSize確定(默認是240M)。使用-XX-TieredCompilation關掉多層編譯,可以減少需要編譯的代碼,從而減少Code Cache的使用。

Compiler:JIT編譯器需要一些內存來才能工作。這個值可以通過關閉多層編譯或減少執行編譯的線程數(-XX:CICompilerCount)來調整.

Class loading:類的元數據(方法的字節碼、符號表、常量池、註解等)被存放在off-heap區域,也叫Metaspace。當前JVM進程加載了越多的類,就會使用越多的metaspace。通過設置-XX:MaxMetaspaceSize(默認是無限)或-XX:CompressedClassSpaceSize(默認是1G)可以限制元空間的大小。

Symbol tables:JVM中維護了兩個重要的哈希表:Symbol表包括類、方法、接口等語言元素的名稱、簽名、ID等,String table記錄了被interned過的字符串的引用。如果Native Tracking表明String table使用了很大的內存,那麼說明該Java應用存在對String.intern方法的濫用。

Threads:線程棧也會使用RAM,棧的大小由-Xss確定。默認是1個線程最大有1M的線程棧,幸運得失事情並沒有這麼糟糕——OS使用惰性策略分配內存頁,實際上每個Java線程使用的RAM很小(一般80~200K),作者使用這個腳本(https://github.com/apangin/jstackmem)來統計有多少RSS空間是屬於Java線程的。

为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

堆外內存(Direct buffers)

Java應用可以通過ByteBuffer.allocateDirect顯式申請堆外內存;默認的堆外內存大小是-Xmx,但是這個值可被-XX:MaxDirectMemorySize覆蓋。在JDK11之前,Direct ByteBuffers被NMT(Native Memory Tracking)列舉在other部分,可以通過JMC觀察到堆外內存的使用情況。

除了DirectByteBuffers,MappedByteBuffers也會使用本地內存,MappedByteBuffers的作用是將文件內容映射到進程的虛擬內存中,NMT沒有跟蹤它們,想要限制這部分的大小並不容易,可以通過pmap -x 命令觀察當前進程使用的實際大小:

<code>1Address Kbytes RSS Dirty Mode Mapping
2...
300007f2b3e557000 39592 32956 0 r--s- some-file-17405-Index.db
400007f2b40c01000 39600 33092 0 r--s- some-file-17404-Index.db
/<code>
为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

本地庫(Native libraries)

由System.loadLibrary加載的JNI代碼也會按需分配RAM,並且這部分內存不受JVM管理。在這裡需要關注的是Java類庫,未關閉的Java資源會導致本地內存洩漏,典型的例子是:ZipInputStream或DirectoryStream。

JVMTI agent,特別是jdwp調試agent,也可能導致內存的過量使用(PS:去年寫memory agent代碼造成的內存洩漏記憶猶新)。

为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

Allocator issues

一個Java進程可以通過系統調用(mmap)或標準庫(malloc)方法來向OS申請內存。malloc自己又通過mmap來向OS申請比較大的內存,並通過自己的算法來管理這些內存,這可能會導致內存碎片,從而導致過量使用虛擬內存。jemalloc是另外一個內存分配器,它比常規的malloc分配器需要更少的footprint,因此可以在自己的C 代碼中嘗試使用jemalloc方法。

为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

結論

無法準確統計一個Java進程使用的虛擬內存,因為有太多因素需要考慮,列舉如下:

<code>1Total memory = Heap Code Cache Metaspace Symbol tables 
2 Other JVM structures Thread stacks
3 Direct buffers Mapped files
4 Native Libraries Malloc overhead .../<code>

作者:CSDN博主「javaadu」,本文首發於作者CSDN博客https://blog.csdn.net/duqi_2009/article/details/101158407。

掃描下方二維碼,下載 CSDN App,查看博主精彩分享

为什么 Java 进程使用的 RAM 比 Heap Size 大?| CSDN博文精选

【END】

CSDN 博客誠邀入駐啦!

本著共享、協作、開源、技術之路我們共同進步的準則,

只要你技術夠乾貨,內容夠紮實,分享夠積極,

歡迎加入 CSDN 大家庭!


分享到:


相關文章: