詳解Native Memory Tracking 追蹤區域分析

来源:https://www.cnblogs.com/huaweiyun/archive/2022/10/24/16821607.html
-Advertisement-
Play Games

摘要:本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。 本文分享自華為雲社區《Native Memory Tracking 詳解(2):追蹤區域分析(一)》,作者:畢昇小助手。 本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap ...


摘要:本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。

本文分享自華為雲社區《Native Memory Tracking 詳解(2):追蹤區域分析(一)》,作者:畢昇小助手。

本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。

追蹤區域記憶體類型

在上文中我們列印了 NMT 的相關報告,但想必大家初次看到報告的時候對其追蹤的各個區域往往都是一頭霧水,下麵就讓我們來簡單認識下各個區域。

查看 JVM 中所設定的記憶體類型:

# hotspot/src/share/vm/memory/allocation.hpp
/*
 * Memory types
 */
enum MemoryType {
 // Memory type by sub systems. It occupies lower byte.
 mtJavaHeap          = 0x00,  // Java heap     //Java 堆
 mtClass             = 0x01,  // memory class for Java classes     //Java classes 使用的記憶體
 mtThread            = 0x02,  // memory for thread objects //線程對象使用的記憶體
 mtThreadStack       = 0x03,  
 mtCode              = 0x04,  // memory for generated code //編譯生成代碼使用的記憶體
 mtGC                = 0x05,  // memory for GC    //GC 使用的記憶體
 mtCompiler          = 0x06,  // memory for compiler  //編譯器使用的記憶體
 mtInternal          = 0x07,  // memory used by VM, but does not belong to    //內部使用的類型
 // any of above categories, and not used for
 // native memory tracking
 mtOther             = 0x08,  // memory not used by VM  //不是 VM 使用的記憶體
 mtSymbol            = 0x09,  // symbol     //符號表使用的記憶體
 mtNMT               = 0x0A,  // memory used by native memory tracking    //NMT 自身使用的記憶體
 mtClassShared       = 0x0B,  // class data sharing  //共用類使用的記憶體
 mtChunk             = 0x0C,  // chunk that holds content of arenas //chunk用於緩存
 mtTest              = 0x0D,  // Test type for verifying NMT
 mtTracing           = 0x0E,  // memory used for Tracing
 mtNone              = 0x0F,  // undefined
 mt_number_of_types  = 0x10   // number of memory types (mtDontTrack
 // is not included as validate type)
};

除去這上面的部分選項,我們發現 NMT 中還有一個 unknown 選項,這主要是在執行 jcmd 命令時,記憶體類別還無法確定或虛擬類型信息還沒有到達時的一些記憶體統計。

Java heap

[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from
    [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 //reserve 記憶體的 call sites
    ......
 [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from
            [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c //commit 記憶體的 call sites
            ......

無需多言,Java 堆使用的記憶體,絕大多數情況下都是 JVM 使用記憶體的主力,堆記憶體通過 mmap 的方式申請。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虛擬地址範圍,因為此時使用的是 G1 垃圾收集器(不是物理意義上的分代),所以無法看到分代地址,如果使用其他物理分代的收集器(如CMS):

[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from
    [0x0000ffffa5cc76d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8
    [0x0000ffffa5c8bf68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0
    [0x0000ffffa570fa10] GenCollectedHeap::allocate(unsigned long, unsigned long*, int*, ReservedSpace*)+0x160
    [0x0000ffffa5711fdc] GenCollectedHeap::initialize()+0x104
        [0x00000000d5550000 - 0x0000000100000000] committed 699072KB from
            [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
            [0x0000ffffa572a450] CardGeneration::CardGeneration(ReservedSpace, unsigned long, int, GenRemSet*)+0xb8
            [0x0000ffffa55dc85c] ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace, unsigned long, int, CardTableRS*, bool, FreeBlockDictionary::DictionaryChoice)+0x54
            [0x0000ffffa572bcdc] GenerationSpec::init(ReservedSpace, int, GenRemSet*)+0xe4
        [0x00000000c0000000 - 0x00000000d5550000] committed 349504KB from
            [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
            [0x0000ffffa5729fe0] Generation::Generation(ReservedSpace, unsigned long, int)+0x98
            [0x0000ffffa5612fa8] DefNewGeneration::DefNewGeneration(ReservedSpace, unsigned long, int, char const*)+0x58
            [0x0000ffffa5b05ec8] ParNewGeneration::ParNewGeneration(ReservedSpace, unsigned long, int)+0x60

我們可以清楚地看到 0x00000000c0000000 - 0x00000000d5550000 為 Java Heap 的新生代(DefNewGeneration)的範圍,0x00000000d5550000 - 0x0000000100000000 為 Java Heap 的老年代(ConcurrentMarkSweepGeneration)的範圍。

  • 我們可以使用 -Xms/-Xmx 或 -XX:InitialHeapSize/-XX:MaxHeapSize 等參數來控制初始/最大的大小,其中基於低停頓的考慮可將兩值設置相等以避免動態擴容縮容帶來的時間開銷(如果基於彈性節約記憶體資源則不必)。
  • 可以如上文所述開啟 -XX:+AlwaysPreTouch 參數強制分配物理記憶體來減少運行時的停頓(如果想要快速啟動進程則不必)。
  • 基於節省記憶體資源還可以啟用 uncommit 機制等。

Class

Class 主要是類元數據(meta data)所使用的記憶體空間,即虛擬機規範中規定的方法區。具體到 HotSpot 的實現中,JDK7 之前是實現在 PermGen 永久代中,JDK8 之後則是移除了 PermGen 變成了 MetaSpace 元空間。

當然以前 PermGen 還有 Interned strings 或者說 StringTable(即字元串常量池),但是 MetaSpace 並不包含 StringTable,在 JDK8 之後 StringTable 就被移入 Heap,並且在 NMT 中 StringTable 所使用的記憶體被單獨統計到了 Symbol 中。

既然 Class 所使用的記憶體用來存放元數據,那麼想必在啟動 JVM 進程的時候設置的 -XX:MaxMetaspaceSize=256M 參數可以限制住 Class 所使用的記憶體大小。

但是我們在查看 NMT 詳情發現一個奇怪的現象:

Class (reserved=1056899KB, committed=4995KB)
                            (classes #442)          //載入的類的數目
                            (malloc=131KB #259) 
                            (mmap: reserved=1056768KB, committed=4864KB)

Class 竟然 reserved 了 1056899KB(約 1G ) 的記憶體,這貌似和我們設定的(256M)不太一樣。

此時我們就不得不簡單補充下相關的內容,我們都知道 JVM 中有一個參數:-XX:UseCompressedOops (簡單來說就是在一定情況下開啟指針壓縮來提升性能),該參數在非 64 位和手動設定 -XX:-UseCompressedOops 的情況下是不會開啟的,而只有在64位系統、不是 client VM、並且 max_heap_size <= max_heap_for_compressed_oops(一個近似32GB的數值)的情況下會預設開啟(計算邏輯可以查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_oops() 方法)。

而如果 -XX:UseCompressedOops 被開啟,並且我們沒有手動設置過 -XX:-UseCompressedClassPointers 的話,JVM 會預設幫我們開啟 UseCompressedClassPointers(詳情可查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_klass_ptrs() 方法)。

我們先忽略 UseCompressedOops 不提,在 UseCompressedClassPointers 被啟動之後,_metadata 的指針就會由 64 位的 Klass 壓縮為 32 位無符號整數值 narrowKlass。簡單看下指向關係:

Java object InstanceKlass 
 [ _mark  ] 
 [ _klass/_narrowKlass ] --> [ ...          ] 
 [ fields ] [ _java_mirror ] 
 [ ...          ] 
(heap) (MetaSpace)

如果我們用的是未壓縮過的 _klass ,那麼使用 64 位指針定址,因此 Klass 可以放置在任意位置;但是如果我們使用壓縮過的 narrowKlass (32位) 進行定址,那麼為了找到該結構實際的 64 位地址,我們不光需要位移操作(如果以 8 位元組對齊左移 3 位),還需要設置一個已知的公共基址,因此限制了我們需要為 Klass 分配為一個連續的記憶體區域。

所以整個 MetaSpace 的記憶體結構在是否開啟 UseCompressedClassPointers 時是不同的:

  • 如果未開啟指針壓縮,那麼 MetaSpace 只有一個 Metaspace Context(incl chunk freelist) 指向很多不同的 virtual space;
  • 如果開啟了指針壓縮,Klass 和非 Klass 部分分開存放,Klass 部分放一個連續的記憶體區域 Metaspace Context(class) (指向一塊大的連續的 virtual space),非 Klass 部分則依照未開啟壓縮的模式放在很多不同的 virtual space 中。這塊 Metaspace Context(class) 記憶體,就是傳說中的 CompressedClassSpaceSize 所設置的記憶體。
//未開啟壓縮
  +--------+  +--------+  +--------+  +--------+
 |  CLD   |  |  CLD   |  |  CLD   |  |  CLD   |
  +--------+  +--------+  +--------+  +--------+
      |           |           |           |       
      |           |           |           |       allocates variable-sized,
      |           |           |           |       typically small-tiny metaspace blocks 
      v           v v v 
  +--------+  +--------+  +--------+  +--------+
  | arena  |  | arena  |  | arena  |  | arena  |
  +--------+  +--------+  +--------+  +--------+
      |           |           |           |       
      |           |           |           |       allocate and, on death, release-in-bulk
      |           |           |           |       medium-sized chunks (1k..4m)
      |           |           |           |       
      v           v v v 
  +--------------------------------------------+
  |                                            |
  |         Metaspace Context                  |
  |          (incl chunk freelist)             |
  |                                            |
  +--------------------------------------------+
         |            |            |
         |            |            |              map/commit/uncommit/release
         |            |            |
         v            v v
    +---------+  +---------+  +---------+
    |         |  |         |  |         |
    | virtual |  | virtual |  | virtual |
    | space |  | space   |  | space   |
    |         |  |         |  |         |
    +---------+  +---------+  +---------+
//開啟了指針壓縮
        +--------+              +--------+
 |  CLD   |              |  CLD   |
        +--------+              +--------+
         /     \                 /     \          Each CLD has two arenas...             
        /       \               /       \       
       /         \             /         \      
      v           v v v 
  +--------+  +--------+  +--------+  +--------+
  | noncl  |  | class  |  | noncl  |  | class  |
  | arena  |  | arena  |  | arena  |  | arena  |
  +--------+  +--------+  +--------+  +--------+
      |              \      /            |       
      |               --------\          |        Non-class arenas take from non-class context,
      |                   /   |          |        class arenas take from class context
      |         /---------    |          |       
      v         v v v 
  +--------------------+  +------------------------+
  |                    |  |                        |
  | Metaspace Context  |  | Metaspace Context      |
  |     (nonclass)     |  |     (class)            |
  |                    |  |                        |
  +--------------------+  +------------------------+
         |            |            |
         |            |            |                    Non-class context: list of smallish mappings
         |            |            |                    Class context: one large mapping (the class space)
         v            v v
  +--------+  +--------+  +----------------~~~~~~~-----+
  |        |  |        |  |                            |
  | virtual|  | virt   |  | virt space (class space)   |
  | space  |  | space  |  |                            |
  |        |  |        |  |                            |
  +--------+  +--------+  +----------------~~~~~~~-----+
MetaSpace相關內容就不再展開描述了,詳情可以參考官方文檔 Metaspace - Metaspace - OpenJDK Wiki () [1] 與 Thomas Stüfe 的系列文章 What is Metaspace? |  [2]。

我們查看 reserve 的具體日誌,發現大部分的記憶體都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申請的,這正是用來分配 CompressedClassSpace 空間的方法:

[0x0000000100000000 - 0x0000000140000000] reserved 1048576KB for Class from
    [0x0000ffff93ea28d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x90
    [0x0000ffff93c16694] Metaspace::allocate_metaspace_compressed_klass_ptrs(char*, unsigned char*)+0x42c
    [0x0000ffff93c16e0c] Metaspace::global_initialize()+0x4fc
    [0x0000ffff93e688a8] universe_init()+0x88

JVM 在初始化 MetaSpace 時,調用鏈路如下:

InitializeJVM ->
Thread::vreate_vm ->
init_globals ->
universe_init ->
MetaSpace::global_initalize ->
Metaspace::allocate_metaspace_compressed_klass_ptrs

查看相關源碼:

# hotspot/src/share/vm/memory/metaspace.cpp
void Metaspace::allocate_metaspace_compressed_klass_ptrs(char* requested_addr, address cds_base) {
  ......
 ReservedSpace metaspace_rs = ReservedSpace(compressed_class_space_size(),
                                             _reserve_alignment,
 large_pages,
 requested_addr, 0);
  ......
 metaspace_rs = ReservedSpace(compressed_class_space_size(),
                                   _reserve_alignment, large_pages);
  ......
}

我們可以發現如果開啟了 UseCompressedClassPointers ,那麼就會調用 allocate_metaspace_compressed_klass_ptrs 方法去 reserve 一個 compressed_class_space_size() 大小的空間(由於我們沒有顯式地設置過 -XX:CompressedClassSpaceSize 的大小,所以此時預設值為 1G)。如果我們顯式地設置 -XX:CompressedClassSpaceSize=256M 再重啟 JVM ,就會發現 reserve 的記憶體大小已經被限制住了:

Thread (reserved=258568KB, committed=258568KB)
                            (thread #127)
                            (stack: reserved=258048KB, committed=258048KB)
                            (malloc=390KB #711)
                            (arena=130KB #234)

但是此時我們不禁會有一個疑問,那就是既然 CompressedClassSpaceSize 可以 reverse 遠遠超過 -XX:MaxMetaspaceSize 設置的大小,那麼 -XX:MaxMetaspaceSize 會不會無法限制住整體 MetaSpace 的大小?實際上 -XX:MaxMetaspaceSize 是可以限制住 MetaSpace 的大小的,只是 HotSpot 此處的代碼順序有問題容易給大家造成誤解和歧義~

查看相關代碼:

# hotspot/src/share/vm/memory/metaspace.cpp
void Metaspace::ergo_initialize() {
  ......
 CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment);
 set_compressed_class_space_size(CompressedClassSpaceSize);
  // Initial virtual space size will be calculated at global_initialize()
 uintx min_metaspace_sz =
      VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize;
 if (UseCompressedClassPointers) {
 if ((min_metaspace_sz + CompressedClassSpaceSize) >  MaxMetaspaceSize) {
 if (min_metaspace_sz >= MaxMetaspaceSize) {
 vm_exit_during_initialization("MaxMetaspaceSize is too small.");
      } else {
        FLAG_SET_ERGO(uintx, CompressedClassSpaceSize,
 MaxMetaspaceSize - min_metaspace_sz);
      }
    }
  } 
......
}

我們可以發現如果 min_metaspace_sz + CompressedClassSpaceSize > MaxMetaspaceSize 的話,JVM 會將 CompressedClassSpaceSize 的值設置為 MaxMetaspaceSize - min_metaspace_sz 的大小,即最後 CompressedClassSpaceSize 的值是小於 MaxMetaspaceSize 的大小的,但是為何之前會 reserve 一個大的值呢?因為在重新計算 CompressedClassSpaceSize 的值之前,JVM 就先調用了 set_compressed_class_space_size 方法將 compressed_class_space_size 的大小設置成了未重新計算的、預設的 CompressedClassSpaceSize 的大小。還記得 compressed_class_space_size 嗎?沒錯,正是我們在上面調用 allocate_metaspace_compressed_klass_ptrs 方法時 reserve 的大小,所以此時 reserve 的其實是一個不正確的值,我們只需要將 set_compressed_class_space_size 的操作放在重新計算 CompressedClassSpaceSize 大小的邏輯之後就能修正這種錯誤。當然因為是 reserve 的記憶體,對真正運行起來的 JVM 並無太大的負面影響,所以沒有人給社區報過這個問題,社區也沒有修改過這一塊邏輯。

如果你使用的 JDK 版本大於等於 10,那麼你直接可以通過 NMT 看到更詳細劃分的 Class 信息(區分了存放 klass 的區域即 Class space、存放非 klass 的區域即 Metadata )。

Class (reserved=1056882KB, committed=1053042KB)
    (classes #483)
    (malloc=114KB #629)
    (mmap: reserved=1056768KB, committed=1052928KB)
 (  Metadata:   )
 (    reserved=8192KB, committed=4352KB)
 (    used=3492KB)
 (   free=860KB)
 (    waste=0KB =0.00%)
 (  Class space:)
 (    reserved=1048576KB, committed=512KB)
 (    used=326KB)
 (   free=186KB)
 (    waste=0KB =0.00%)

Thread

線程所使用的記憶體:

Thread (reserved=258568KB, committed=258568KB)
                            (thread #127)           //線程個數
                            (stack: reserved=258048KB, committed=258048KB)   //棧使用的記憶體
                            (malloc=390KB #711) 
                            (arena=130KB #234)      //線程句柄使用的記憶體
    ......
    [0x0000fffdbea32000 - 0x0000fffdbec32000] reserved and committed 2048KB for Thread Stack from
    [0x0000ffff935ab79c] attach_listener_thread_entry(JavaThread*, Thread*)+0x34
    [0x0000ffff93e3ddb4] JavaThread::thread_main_inner()+0xf4
    [0x0000ffff93e3e01c] JavaThread::run()+0x214
    [0x0000ffff93cb49e4] java_start(Thread*)+0x11c
[0x0000fffdbecce000 - 0x0000fffdbeece000] reserved and committed 2048KB for Thread Stack from
    [0x0000ffff93cb49e4] java_start(Thread*)+0x11c
    [0x0000ffff944148bc] start_thread+0x19c

觀察 NMT 列印信息,我們可以發現,此時的 JVM 進程共使用了127個線程,committed 了 258568KB 的記憶體。

繼續觀察下麵各個線程的分配情況就會發現,每個線程 committed 了2048KB(2M)的記憶體空間,這可能和平時的認知不太相同,因為平時我們大多數情況下使用的都是x86平臺,而筆者此時使用的是 ARM (aarch64)的平臺,所以此處線程預設分配的記憶體與 x86 不同。

如果我們不顯式的設置 -Xss/-XX:ThreadStackSize 相關的參數,那麼 JVM 會使用預設的值。

在 aarch64 平臺下預設為 2M:

# globals_linux_aarch64.hpp
define_pd_global(intx, ThreadStackSize,          2048); // 0 => use system default
define_pd_global(intx, VMThreadStackSize,        2048);

而在 x86 平臺下預設為 1M:

# globals_linux_x86.hpp
define_pd_global(intx, ThreadStackSize,          1024); // 0 => use system default
define_pd_global(intx, VMThreadStackSize,        1024);

如果我們想縮減此部分記憶體的使用,可以使用參數 -Xss/-XX:ThreadStackSize 設置適合自身業務情況的大小,但是需要進行相關壓力測試保證不會出現溢出等錯誤。

Code

JVM 自身會生成一些 native code 並將其存儲在稱為 codecache 的記憶體區域中。JVM 生成 native code 的原因有很多,包括動態生成的解釋器迴圈、 JNI、即時編譯器(JIT)編譯 Java 方法生成的本機代碼 。其中 JIT 生成的 native code 占據了 codecache 絕大部分的空間。

Code (reserved=266273KB, committed=4001KB)
                            (malloc=33KB #309) 
                            (mmap: reserved=266240KB, committed=3968KB)
    ......
    [0x0000ffff7c000000 - 0x0000ffff8c000000] reserved 262144KB for Code from
    [0x0000ffff93ea3c2c] ReservedCodeSpace::ReservedCodeSpace(unsigned long, unsigned long, bool)+0x84
    [0x0000ffff9392dcd0] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xc8
    [0x0000ffff9374bd64] codeCache_init()+0xb4
    [0x0000ffff9395ced0] init_globals()+0x58
 [0x0000ffff7c3c0000 - 0x0000ffff7c3d0000] committed 64KB from
            [0x0000ffff93ea47e0] VirtualSpace::expand_by(unsigned long, bool)+0x1d8
            [0x0000ffff9392e01c] CodeHeap::expand_by(unsigned long)+0xac
            [0x0000ffff9374cee4] CodeCache::allocate(int, bool)+0x64
            [0x0000ffff937444b8] MethodHandlesAdapterBlob::create(int)+0xa8

追蹤 codecache 的邏輯:

# codeCache.cpp
void CodeCache::initialize() {
  ......
 CodeCacheExpansionSize = round_to(CodeCacheExpansionSize, os::vm_page_size());
 InitialCodeCacheSize = round_to(InitialCodeCacheSize, os::vm_page_size());
 ReservedCodeCacheSize = round_to(ReservedCodeCacheSize, os::vm_page_size());
 if (!_heap->reserve(ReservedCodeCacheSize, InitialCodeCacheSize, CodeCacheSegmentSize)) {
 vm_exit_during_initialization("Could not reserve enough space for code cache");
  }
  ......
}
# virtualspace.cpp
//記錄 mtCode 的函數,其中 r_size 由 ReservedCodeCacheSize 得出
ReservedCodeSpace::ReservedCodeSpace(size_t r_size,
 size_t rs_align,
 bool large) :
 ReservedSpace(r_size, rs_align, large, /*executable*/ true) {
 MemTracker::record_virtual_memory_type((address)base(), mtCode);
}

可以發現 CodeCache::initialize() 時 codecache reserve 的最大記憶體是由我們設置的 -XX:ReservedCodeCacheSize 參數決定的(當然 ReservedCodeCacheSize 的值會做一些對齊操作),我們可以通過設置 -XX:ReservedCodeCacheSize 來限制 Code 相關的最大記憶體。

同時我們發現,初始化時 codecache commit 的記憶體可以由 -XX:InitialCodeCacheSize 參數來控制,具體計算代碼可以查看 VirtualSpace::expand_by 函數。

我們設置 -XX:InitialCodeCacheSize=128M 後重啟 JVM 進程,再次查看 NMT detail:

Code (reserved=266273KB, committed=133153KB)
                            (malloc=33KB #309) 
                            (mmap: reserved=266240KB, committed=133120KB) 
    ......
 [0x0000ffff80000000 - 0x0000ffff88000000] committed 131072KB from
            [0x0000ffff979e60e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
            [0x0000ffff9746fcfc] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xf4
            [0x0000ffff9728dd64] codeCache_init()+0xb4
            [0x0000ffff9749eed0] init_globals()+0x58

我們可以通過 -XX:InitialCodeCacheSize 來設置 codecache 初始 commit 的記憶體。

  • 除了使用 NMT 列印codecache相關信息,我們還可以通過 -XX:+PrintCodeCache(JVM 關閉時輸出codecache的使用情況)和jcmdpidCompiler.codecache(只有在 JDK 9 及以上版本的jcmd才支持該選項)來查看codecache相關的信息。
  • 瞭解更多codecache詳情可以查看CodeCache官方文檔 [3]。

GC

GC 所使用的記憶體,就是垃圾收集器使用的數據所占據的記憶體,例如卡表 card tables、記憶集 remembered sets、標記棧 marking stack、標記點陣圖 marking bitmaps 等等。其實不論是 card tables、remembered sets 還是 marking stack、marking bitmaps,都是一種藉助額外的空間,來記錄不同記憶體區域之間引用關係的結構(都是基於空間換時間的思想,否則尋找引用關係就需要諸如遍歷這種浪費時間的方式)。

簡單介紹下相關概念:

更詳細的信息不深入展開介紹了,可以查看彭成寒老師《JVM G1源碼分析和調優》2.3 章 [4] 與 4.1 章節 [5],還可以查看 R大(RednaxelaFX)對相關概念的科普 [6]。
  • 卡表 card tables,在部分收集器(如CMS)中存儲跨代引用(如老年代中對象指向年輕代的對象)的數據結構,精度可以有很多種選擇:

如果精確到機器字,那麼往往描述的區域太小了,使用的記憶體開銷會變大,所以 HotSpot 中選擇 512KB 為精度大小。

卡表甚至可以細到和 bitmap 相同,即使用 1 bit 位來對應一個記憶體頁(512KB),但是因為 JVM 在操作一個 bit 位時,仍然需要讀取整個機器字 word,並且操作 bit 位的開銷有時反而大於操作 byte 。所以 HotSpot 的 cardTable 選擇使用 byte 數組代替 bit ,用 1 byte 對應 512KB 的空間,使用 byte 數組的開銷也可以接受(1G 的堆記憶體使用卡表也只占用2M:1 * 1024 * 1024 / 512 = 2048 KB)。

我們以 cardTableModRefBS 為例,查看其源碼結構:

# hotspor/src/share/vm/momery/cardTableModRefBS.hpp
//精度為 512 KB
enum SomePublicConstants {
 card_shift                  = 9,
 card_size                   = 1 << card_shift,
 card_size_in_words          = card_size / sizeof(HeapWord)
};
......
class CardTableModRefBS: public ModRefBarrierSet {
    .....
 size_t          _byte_map_size;    // in bytes
 jbyte*          _byte_map;    // the card marking array
    .....
}

可以發現 cardTableModRefBS 通過枚舉 SomePublicConstants 來定義對應的記憶體塊 card_size 的大小即:512KB,而 _byte_map 則是用於標記的卡表位元組數組,我們可以看到其對應的類型為 jbyte(typedef signed char jbyte,其實就是一個位元組即 1byte)。

當然後來卡表不只記錄跨代引用的關係,還會被 CMS 的增量更新之類的操作復用。
    • 字粒度:精確到機器字(word),該字包含有跨代指針。
    • 對象粒度:精確到一個對象,該對象里有欄位含有跨代指針。
    • card粒度:精確到一大塊記憶體區域,該區域內有對象含有跨代指針。

記憶集 remembered sets,可以選擇的粒度和卡表差不多,或者你說卡表也是記憶集的一種實現方式也可以(區別可以查看上面給出的 R大的鏈接)。G1 中引入記憶集 RSet 來記錄 Region 間的跨代引用,G1 中的卡表的作用並不是記錄引用關係,而是用於記錄該區域中對象垃圾回收過程中的狀態信息。

標記棧 marking stack,初始標記掃描根集合時,會標記所有從根集合可直接到達的對象並將它們的欄位壓入掃描棧(marking stack)中等待後續掃描。

標記點陣圖 marking bitmaps,我們常使用點陣圖來指示哪塊記憶體已經使用、哪塊記憶體還未使用。比如 G1 中的 Mixed GC 混合收集演算法(收集所有的年輕代的 Region,外加根據global concurrent marking 統計得出的收集收益高的部分老年代 Region)中用到了併發標記,併發標記就引入兩個點陣圖 PrevBitMap 和 NextBitMap,用這兩個點陣圖來輔助標記併發標記不同階段記憶體的使用狀態。

查看 NMT 詳情:

......
    [0x0000fffe16000000 - 0x0000fffe17000000] reserved 16384KB for GC from
        [0x0000ffff93ea2718] ReservedSpace::ReservedSpace(unsigned long, unsigned long)+0x118
        [0x0000ffff93892328] G1CollectedHeap::create_aux_memory_mapper(char const*, unsigned long, unsigned long)+0x48
        [0x0000ffff93899108] G1CollectedHeap::initialize()+0x368
        [0x0000ffff93e68594] Universe::initialize_heap()+0x15c
        [0x0000fffe16000000 - 0x0000fffe17000000] committed 16384KB from
                [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c
                [0x0000ffff938bc08c] G1PageBasedVirtualSpace::commit(unsigned long, unsigned long)+0x11c
                [0x0000ffff938bf774] G1RegionsLargerThanCommitSizeMapper::commit_regions(unsigned int, unsigned long)+0x5c
                [0x0000ffff93943f8c] HeapRegionManager::commit_regions(unsigned int, unsigned long)+0xb4
......            

我們可以發現 JVM 在初始化 heap 堆的時候(此時是 G1 收集器所使用的堆 G1CollectedHeap),不僅會創建 remember set ,還會有一個 create_aux_memory_mapper 的操作,用來給 GC 輔助用的數據結構(如:card table、prev bitmap、 next bitmap 等)創建對應的記憶體映射,相關操作可以查看 g1CollectedHeap 初始化部分源代碼:

# hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
jint G1CollectedHeap::initialize() {
  ......
 //創建 G1 remember set
 // Also create a G1 rem set.
  _g1_rem_set = new G1RemSet(this, g1_barrier_set());
  ......
 // Create storage for the BOT, card table, card counts table (hot card cache) and the bitmaps.
  G1RegionToSpaceMapper* bot_storage =
 create_aux_memory_mapper("Block offset table",
 G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize),
 G1BlockOffsetSharedArray::N_bytes);
 ReservedSpace cardtable_rs(G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize));
  G1RegionToSpaceMapper* cardtable_storage =
 create_aux_memory_mapper("Card table",
 G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize),
 G1BlockOffsetSharedArray::N_bytes);
  G1RegionToSpaceMapper* card_counts_storage =
 create_aux_memory_mapper("Card counts table",
 G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize),
 G1BlockOffsetSharedArray::N_bytes);
 size_t bitmap_size = CMBitMap::compute_size(g1_rs.size());
  G1RegionToSpaceMapper* prev_bitmap_storage =
 create_aux_memory_mapper("Prev Bitmap", bitmap_size, CMBitMap::mark_distance());
  G1RegionToSpaceMapper* next_bitmap_storage =
 create_aux_memory_mapper("Next Bitmap", bitmap_size, CMBitMap::mark_distance());
  _hrm.initialize(heap_storage, prev_bitmap_storage, next_bitmap_storage, bot_storage, cardtable_storage, card_counts_storage);
  g1_barrier_set()->initialize(cardtable_storage);
 // Do later initialization work for concurrent refinement.
  _cg1r->init(card_counts_storage);
  ......
}

因為這些輔助的結構都是一種空間換時間的思想,所以不可避免的會占用額外的記憶體,尤其是 G1 的 RSet 結構,當我們調大我們的堆記憶體,GC 所使用的記憶體也會不可避免的跟隨增長:

# -Xmx1G -Xms1G
GC (reserved=164403KB, committed=164403KB)
                            (malloc=92723KB #6540) 
                            (mmap: reserved=71680KB, committed=71680KB)
# -Xmx2G -Xms2G
GC (reserved=207891KB, committed=207891KB)
                            (malloc=97299KB #12683)
                            (mmap: reserved=110592KB, committed=110592KB)
# -Xmx4G -Xms4G
GC (reserved=290313KB, committed=290313KB)
                            (malloc=101897KB #12680)
                            (mmap: reserved=188416KB, committed=188416KB)
# -Xmx8G -Xms8G
GC (reserved=446473KB, committed=446473KB)
                            (malloc=102409KB #12680)
                            (mmap: reserved=344064KB, committed=344064KB)

我們可以看到這個額外的記憶體開銷一般在 1% - 20%之間,當然如果我們不使用 G1 收集器,這個開銷是沒有那麼大的:

# -XX:+UseSerialGC -Xmx8G -Xms8G
GC (reserved=27319KB, committed=27319KB)
                            (malloc=7KB #79)
                            (mmap: reserved=27312KB, committed=27312KB)
# -XX:+UseConcMarkSweepGC -Xmx8G -Xms8G 
GC (reserved=167318KB, committed=167318KB)
                            (malloc=140006KB #373)
                            (mmap: reserved=27312KB, committed=27312KB)

我們可以看到,使用最輕量級的 UseSerialGC,GC 部分占用的記憶體有很明顯的降低(436M -> 26.67M);使用 CMS ,GC 部分從 436M 降低到 163.39M。

GC 這塊記憶體是必須的,也是我們在使用過程中無法壓縮的。停頓、吞吐量、記憶體占用就是 GC 中不可能同時達到的三元悖論,不同的垃圾收集器在這三者中有不同的側重,我們應該結合自身的業務情況綜合考量選擇合適的垃圾收集器。

由於篇幅有限,將在下篇文章繼續給大家分享 追蹤區域的其它記憶體類型(包含Compiler、Internal、Symbol、Native Memory Tracking、Arena Chunk 和 Unknown)以及 NMT 無法追蹤的記憶體,敬請期待!

參考

  1. https://wiki.openjdk.java.net/display/HotSpot/Metaspace
  2. https://stuefe.de/posts/metaspace/what-is-metaspace
  3. https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm
  4. https://weread.qq.com/web/reader/53032310717f44515302749k3c5327902153c59dc0488e1
  5. https://weread.qq.com/web/reader/53032310717f44515302749ka1d32a6022aa1d0c6e83eb4
  6. https://hllvm-group.iteye.com/group/topic/21468#post-272070

歡迎加入Compiler SIG交流群與大家共同交流學習編譯技術相關內容,掃碼添加小助手微信邀請你進入Compiler SIG交流群。

 

點擊關註,第一時間瞭解華為雲新鮮技術~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄 一.OpenGL 反色 1.IOS Object-C 版本 2.Windows OpenGL ES 版本 3.Windows OpenGL 版本 二.OpenGL 反色 GLSL Shader 三.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> Op ...
  • 編程教材 《R語言實戰·第2版》Robert I. Kabacoff 課程教材《商務與經濟統計·原書第13版》 (安德森) P143、案例 Go Bananas #1 生產中斷的概率 c <- pbinom(4, 25, .08) # 4 是預設 P(x <= 4) answer1 <- 1 - c ...
  • Java 一直是一種面向對象的編程語言。這意味著 Java 編程中的一切都圍繞著對象(為了簡單起見,除了一些基本類型)。我們不僅有 Java 中的函數,它們還是 Class 的一部分,我們需要使用 class/object 來調用任何函數。 函數式介面 當我們研究一些其他的編程語言時,比如C++,J ...
  • 在筆者前面有一篇文章`《驅動開發:斷鏈隱藏驅動程式自身》`通過摘除驅動的鏈表實現了斷鏈隱藏自身的目的,但此方法恢復時會觸發PG會藍屏,偶然間在網上找到了一個作者介紹的一種方法,覺得有必要詳細分析一下他是如何實現的進程隱藏的,總體來說作者的思路是最終尋找到`MiProcessLoaderEntry`的... ...
  • 前言 一. 數據來源分析 明確需求, 我們採集網上什麼數據內容, 在什麼地方 分析我們想要高清原圖在什麼地方有 瀏覽器自帶工具: 開發者工具 F12 滑鼠右鍵點擊 插件 選擇 network 刷新網頁 點擊選擇 Img 可以直接找到圖片地址 通過搜索分析, 可以知道, 我們想要圖片原圖url 就在 ...
  • Eclipse插件開發的點點滴滴 新公司做的是桌面應用程式, 與之前一直在做的web頁面 ,相差甚大 。 這篇文章是寫於2022年10月底,這時在新公司已經入職了快三月。寫作目的是:國內對於eclipse插件開發相關的文檔是少之又少,這三個月我們小組翻遍了國外文檔,勉強將軟體拼湊出並release出 ...
  • @ 起因 近期身邊的一位朋友來尋求幫助,她在日常工作時,總是需要做一些重覆的事情,所以想著是否能通過程式實現自動化的操作。 具體需求為,每天會收到一份固定格式的Word文件,然後根據其中的內容,填充到固定的PPT模板中,最終生成圖片輸出。 過程 確定工具 有了需求後,第一件事自然是在網路上查找是否有 ...
  • django原生api介面 1.1 創建django項目 django-admin startproject drfdemo1 1.2 創建app django-admin startapp app 1.3 創建數據模型 app/models.py中編寫如下代碼: from django.db im ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...