主頁 > 後端開發 > 詳解Native Memory Tracking 追蹤區域分析

詳解Native Memory Tracking 追蹤區域分析

2022-10-25 07:11:01 後端開發

摘要:本篇將介紹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 (http://java.net) [1] 與 Thomas Stüfe 的系列文章 What is Metaspace? | http://stuefe.de [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交流群,

 

點擊關注,第一時間了解華為云新鮮技術~

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/519116.html

標籤:其他

上一篇:在Mac M1 Pro 芯片上安裝PHP擴展及例外處理 - Swoole

下一篇:Java Apache POI 小記(讀取Word通過模板創建PPT)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more