Explication détaillée de l'analyse de la zone de suivi du suivi de la mémoire native

Résumé : Cet article présentera certains types de mémoire de la zone de suivi NMT - tas Java, classe, thread, code et GC.

Cet article est partagé par la communauté HUAWEI CLOUD " Explication détaillée du suivi de la mémoire native (2) : Analyse de la zone de suivi (1) ", auteur : Assistant Bi Sheng.

Cet article présentera certains types de mémoire de la zone de suivi NMT - tas Java, classe, thread, code et GC.

Type de mémoire de zone de trace

Dans ce qui précède, nous avons imprimé les rapports pertinents de NMT, mais on peut supposer que lorsque vous voyez le rapport pour la première fois, vous êtes souvent confus quant aux domaines qu'il suit. Laissez-nous comprendre brièvement chaque domaine.

Vérifiez le type de mémoire défini dans la 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)
};

En supprimant certaines des options ci-dessus, nous avons constaté qu'il existe également une option inconnue dans NMT, qui consiste principalement en des statistiques de mémoire lorsque la catégorie de mémoire ne peut pas être déterminée ou que les informations de type virtuel ne sont pas arrivées lorsque la commande jcmd est exécutée.

tas Java

[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
            ......

Inutile de dire que la mémoire utilisée par le tas Java est, dans la plupart des cas, la principale force utilisée par la JVM, et la mémoire du tas est sollicitée via mmap. 0x00000000c0000000 - 0x0000000100000000 est la plage d'adresses virtuelles de Java Heap, car le ramasse-miettes G1 (et non la génération au sens physique) est utilisé à ce moment-là, de sorte que l'adresse de génération ne peut pas être vue. Si vous utilisez d'autres collecteurs de génération physiques (tels que 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

Nous pouvons clairement voir que 0x00000000c0000000 - 0x00000000d5550000 est la plage de la nouvelle génération (DefNewGeneration) du tas Java, et 0x00000000d5550000-0x0000000100000000 est la plage de l'ancienne génération (ConcurrentMarkSweepGeneration) du tas Java.

  • Nous pouvons utiliser des paramètres tels que -Xms/-Xmx ou -XX:InitialHeapSize/-XX:MaxHeapSize pour contrôler la taille initiale/maximale. En tenant compte de la faible pause, les deux valeurs peuvent être égales pour éviter le temps causée par l'expansion et la contraction dynamiques Surcharge (inutile si l'économie de ressources mémoire est basée sur l'élasticité).
  • Vous pouvez activer le paramètre -XX:+AlwaysPreTouch comme décrit ci-dessus pour forcer l'allocation de mémoire physique afin de réduire les arrêts d'exécution (pas nécessaire si vous souhaitez démarrer le processus rapidement).
  • Le mécanisme de désengagement peut également être activé en fonction de l'économie des ressources mémoire.

Classer

La classe est principalement l'espace mémoire utilisé par les métadonnées de classe (métadonnées), c'est-à-dire la zone de méthode spécifiée dans la spécification de la machine virtuelle. Plus précisément dans l'implémentation de HotSpot, JDK7 a été implémenté dans la génération permanente PermGen avant, et après JDK8, PermGen a été supprimé et est devenu le métaspace MetaSpace.

Bien sûr, PermGen avait des chaînes internes ou StringTable (c'est-à-dire un pool de constantes de chaînes), mais MetaSpace ne contenait pas StringTable.Après JDK8, StringTable a été déplacé dans Heap et la mémoire utilisée par StringTable dans NMT a été comptée séparément dans Symbol.                    

Étant donné que la mémoire utilisée par Class est utilisée pour stocker les métadonnées, le paramètre -XX:MaxMetaspaceSize=256M défini lors du démarrage du processus JVM doit limiter la taille de la mémoire utilisée par Class.

Mais nous avons trouvé un phénomène étrange en regardant les détails NMT :

Class (reserved=1056899KB, committed=4995KB)
                            (classes #442)          //加载的类的数目
                            (malloc=131KB #259) 
                            (mmap: reserved=1056768KB, committed=4864KB)

La classe a en fait réservé 1056899 Ko (environ 1 Go) de mémoire, ce qui semble être différent de ce que nous avons défini (256 Mo).

A ce stade, nous devons simplement ajouter du contenu pertinent. Nous savons tous qu'il existe un paramètre dans la JVM : dans les paramètres non 64 bits et manuels -XX:-UseCompressedOops ne sera pas activé, mais ne sera activé par défaut que sur un système 64 bits, pas sur une machine virtuelle cliente, et max_heap_size <= max_heap_for_compressed_oops (une valeur d'environ 32 Go) ( la logique de calcul peut être Voir la méthode Arguments::set_use_compressed_oops() dans hotspot/src/share/vm/runtime/arguments.cpp).

Et si -XX:UseCompressedOops est activé, et que nous n'avons pas défini manuellement -XX:-UseCompressedClassPointers, la JVM activera UseCompressedClassPointers pour nous par défaut (voir Arguments in hotspot/src/share/vm/runtime/arguments.cpp pour plus de détails : :méthode set_use_compressed_klass_ptrs()).

Ignorons d'abord UseCompressedOops. Une fois UseCompressedClassPointers activé, le pointeur _metadata sera compressé de Klass 64 bits à une valeur entière non signée 32 bits tightKlass. Regardez simplement la relation de pointage:

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

Si nous utilisons _klass non compressé, nous utilisons l'adressage de pointeur 64 bits, donc Klass peut être placé n'importe où; mais si nous utilisons l'adressage compressé étroit Klass (32 bits), alors pour trouver la structure en fait l'adresse 64 bits , nous devons non seulement décaler l'opération (si l'alignement de 8 octets décale à gauche de 3 bits), mais également définir une adresse de base commune connue, nous devons donc allouer Klass en tant que zone de mémoire contiguë.

Ainsi, la structure de la mémoire de l'ensemble du MetaSpace est différente lorsque UseCompressedClassPointers est activé ou non :

  • Si la compression du pointeur n'est pas activée, alors le MetaSpace n'a qu'un seul contexte de métaspace (incl chunk freelist) pointant vers de nombreux espaces virtuels différents ;
  • Si la compression du pointeur est activée, les parties Klass et non-Klass sont stockées séparément. La partie Klass est placée dans une zone de mémoire continue Metaspace Context(class) (pointant vers un grand espace virtuel contigu), et la partie non-Klass est placée en mode non compressé dans de nombreux espaces virtuels différents. Cette mémoire Metaspace Context(class) est la mémoire définie par le légendaire 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  |  |                            |
  |        |  |        |  |                            |
  +--------+  +--------+  +----------------~~~~~~~-----+
Le contenu lié à MetaSpace ne sera plus décrit. Pour plus de détails, veuillez vous référer au document officiel Metaspace - Metaspace - OpenJDK Wiki ( http://java.net ) [1] et à la série d'articles de Thomas Stüfe Qu'est-ce que Metaspace ? | http : // stuefe . de [2].       

Nous avons examiné le journal spécifique de la réserve et constaté que la majeure partie de la mémoire était demandée par la méthode Metaspace :: allocate_metaspace_compressed_klass_ptrs, qui est exactement la méthode utilisée pour allouer l'espace 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

Lorsque la JVM initialise MetaSpace, la chaîne d'appel est la suivante :

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

Afficher le code source associé :

# 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);
  ......
}

Nous pouvons constater que si UseCompressedClassPointers est activé, la méthode allow_metaspace_compressed_klass_ptrs sera appelée pour réserver un espace de taillepressed_class_space_size() (puisque nous n'avons pas défini explicitement la taille de -XX:CompressedClassSpaceSize, la valeur par défaut est 1G pour le moment). Si nous définissons explicitement -XX:CompressedClassSpaceSize=256M et redémarrons la JVM, nous constaterons que la taille de la mémoire de la réserve a été limitée :

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

Mais pour le moment, nous ne pouvons pas nous empêcher d'avoir une question, c'est-à-dire que, puisque CompressedClassSpaceSize peut inverser bien plus que la taille définie par -XX:MaxMetaspaceSize, est-ce que -XX:MaxMetaspaceSize limitera la taille du MetaSpace global ? En fait, -XX:MaxMetaspaceSize peut limiter la taille de MetaSpace, mais il y a un problème avec l'ordre du code ici dans HotSpot, ce qui peut facilement provoquer des malentendus et des ambiguïtés pour tout le monde~

Voir le code associé :

# 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);
      }
    }
  } 
......
}

Nous pouvons constater que si min_metaspace_sz + CompressedClassSpaceSize > MaxMetaspaceSize, la JVM définira la valeur de CompressedClassSpaceSize à la taille de MaxMetaspaceSize - min_metaspace_sz, c'est-à-dire que la valeur finale de CompressedClassSpaceSize est inférieure à la taille de MaxMetaspaceSize, mais pourquoi réserver une grande valeur avant ? Parce qu'avant de recalculer la valeur de CompressedClassSpaceSize, la JVM appelle la méthode set_compressed_class_space_size pour définir la taille depressed_class_space_size sur la taille par défaut non recalculée de CompressedClassSpaceSize. Vous vous souvenez depressed_class_space_size ? C'est vrai, c'est la taille de la réserve lorsque nous appelons la méthode allow_metaspace_compressed_klass_ptrs ci-dessus, donc la valeur réservée est en fait une valeur incorrecte. Il suffit de mettre l'opération de set_compressed_class_space_size dans la logique de recalcul de la taille de CompressedClassSpaceSize pour corriger cette erreur . Bien sûr, comme il s'agit de mémoire réservée, cela n'a pas beaucoup d'impact négatif sur la vraie JVM en cours d'exécution, donc personne n'a signalé ce problème à la communauté, et la communauté n'a pas modifié ce morceau de logique.

Si la version de JDK que vous utilisez est supérieure ou égale à 10, vous pouvez voir directement les informations de classe plus détaillées via NMT (distinguer la zone où klass est stocké, à savoir l'espace de classe, et la zone où non-klass est stocké, à savoir les métadonnées).

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%)

Fil de discussion

Mémoire utilisée par les threads :

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

En regardant les informations d'impression NMT, nous pouvons constater que le processus JVM à ce moment a utilisé un total de 127 threads et engagé 258568 Ko de mémoire.

En continuant à observer l'allocation des threads suivants, vous constaterez que chaque thread a engagé 2048 Ko (2M) d'espace mémoire, ce qui peut être différent de la cognition habituelle, car nous utilisons généralement la plate-forme x86 dans la plupart des cas. la plate-forme ARM (aarch64) à ce moment, donc la mémoire par défaut allouée par le thread ici est différente de celle de x86.

Si nous ne définissons pas explicitement les paramètres liés à -Xss/-XX:ThreadStackSize, la JVM utilisera la valeur par défaut.

La valeur par défaut est 2M sous la plate-forme aarch64 :

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

Sur la plate-forme x86, la valeur par défaut est 1M :

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

Si nous voulons réduire l'utilisation de cette partie de la mémoire, nous pouvons utiliser le paramètre -Xss/-XX:ThreadStackSize pour définir la taille adaptée à notre propre situation commerciale, mais nous devons effectuer des tests de résistance pertinents pour nous assurer qu'il y aura pas d'erreurs telles que le débordement.

Code

La JVM elle-même génère du code natif et le stocke dans une zone mémoire appelée codecache. La JVM génère du code natif pour un certain nombre de raisons, notamment des boucles d'interprétation générées dynamiquement, JNI et la compilation juste-à-temps (JIT) du code natif généré par les méthodes Java. Le code natif généré par JIT occupe la majeure partie de l'espace du 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

Tracez la logique du 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);
}

On peut constater que la mémoire maximale réservée par codecache pendant CodeCache::initialize() est déterminée par le paramètre -XX:ReservedCodeCacheSize que nous avons défini (bien sûr, la valeur de ReservedCodeCacheSize fera certaines opérations d'alignement), nous pouvons limiter le code lié en définissant -XX:ReservedCodeCacheSize mémoire maximale.

Dans le même temps, nous avons constaté que la mémoire de la validation du cache de code lors de l'initialisation peut être contrôlée par le paramètre -XX:InitialCodeCacheSize, et que le code de calcul spécifique peut être visualisé dans la fonction VirtualSpace::expand_by.

Nous définissons -XX:InitialCodeCacheSize=128M et redémarrons le processus JVM, et vérifions à nouveau les détails NMT :

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

Nous pouvons définir la mémoire de validation initiale du cache de code par -XX: InitialCodeCacheSize.

  • En plus d'utiliser NMT pour imprimer les informations relatives au cache de code , nous pouvons également utiliser -XX:+PrintCodeCache (utilisation du cache de code de sortie lorsque la JVM est fermée) et jcmd pid Compiler.codecache (uniquement dans JDK 9 et les versions supérieures de jcmd prennent en charge cette option) pour afficher les informations relatives au code cache .
  • Pour plus de détails sur le codecache, veuillez vous référer à la documentation officielle de CodeCache [3].

CG

La mémoire utilisée par GC est la mémoire occupée par les données utilisées par le ramasse-miettes, telles que les tables de cartes, les ensembles mémorisés, la pile de marquage, les bitmaps de marquage, etc. En fait, qu'il s'agisse de tables de cartes, d'ensembles mémorisés, de piles de marquage ou de bitmaps de marquage, il s'agit d'une structure qui utilise de l'espace supplémentaire pour enregistrer la relation de référence entre différentes zones de mémoire (toutes sont basées sur l'idée de changer d'espace pour le temps, sinon trouver la relation de référence sera difficile (des moyens de perte de temps tels que le parcours sont nécessaires).

Présentez brièvement les concepts pertinents :

Des informations plus détaillées ne seront pas introduites en profondeur. Vous pouvez consulter les chapitres 2.3 [4] et 4.1 [5] de "JVM G1 Source Code Analysis and Tuning" de M. Peng Chenghan, et vous pouvez également consulter la vulgarisation scientifique des concepts connexes. dans R (RednaxelaFX) [6] ].
  • Les tables à cartes sont des structures de données qui stockent des références intergénérationnelles (comme des objets de l'ancienne génération pointant vers des objets de la jeune génération) dans certains collecteurs (comme les CMS).Il existe de nombreux choix de précision :

S'il est précis pour les mots machine, la zone décrite est souvent trop petite et la surcharge de mémoire utilisée deviendra plus grande, donc 512 Ko est sélectionné comme taille de précision dans HotSpot.

La table de la carte peut même être aussi détaillée que le bitmap, c'est-à-dire utiliser 1 bit pour correspondre à une page mémoire (512 Ko), mais parce que la JVM doit toujours lire l'intégralité du mot machine lors de l'utilisation d'un bit, et la surcharge d'exploitation le bit est parfois Au lieu de cela, il est supérieur à l'octet d'opération. Ainsi, la table cardTable de HotSpot choisit d'utiliser un tableau d'octets au lieu de bit, 1 octet correspond à 512 Ko d'espace et la surcharge de l'utilisation d'un tableau d'octets est acceptable (la mémoire de tas 1G n'utilise que 2M pour la table de cartes : 1 * 1024 * 1024 / 512 = 2048 Ko ).

Prenons cardTableModRefBS comme exemple pour voir sa structure de code source :

# 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
    .....
}

On peut constater que cardTableModRefBS définit la taille du bloc mémoire correspondant card_size en énumérant SomePublicConstants : 512 Ko, et _byte_map est le tableau d'octets de la table des cartes utilisé pour le marquage, on peut voir que son type correspondant est jbyte (typedef signé char jbyte , en fait , c'est un octet ou 1 octet).

Bien sûr, plus tard, la table de cartes enregistre non seulement la relation des références de génération croisée, mais est également réutilisée par des opérations telles que des mises à jour incrémentielles du CMS.
    • Granularité des mots : précise à un mot machine (mot), qui contient un pointeur intergénérationnel.
    • Granularité de l'objet : précis pour un objet dont les champs contiennent des pointeurs intergénérationnels.
    • Granularité de la carte : précise jusqu'à une grande zone de mémoire où les objets contiennent des pointeurs de génération croisée.

Pour les ensembles mémorisés, la granularité qui peut être sélectionnée est similaire à celle de la table de cartes, ou vous pouvez dire que la table de cartes est également une implémentation de l'ensemble de mémoire (la différence peut être trouvée dans le lien R donné ci-dessus). L'ensemble de mémoire RSet est introduit dans G1 pour enregistrer les références intergénérationnelles entre les régions. La fonction de la table de cartes dans G1 n'est pas d'enregistrer la relation de référence, mais d'enregistrer les informations d'état des objets dans la région lors de la collecte des ordures. traiter.

Pile de marquage pile de marquage, lorsque la marque initiale scanne la collection racine, elle marquera tous les objets directement accessibles à partir de la collection racine et poussera leurs champs dans la pile de numérisation (pile de marquage) pour une numérisation ultérieure.

Marquage des bitmaps Marquage des bitmaps, nous utilisons souvent des bitmaps pour indiquer quelle mémoire a été utilisée et quelle mémoire n'a pas été utilisée. Par exemple, l'algorithme de collecte hybride GC mixte dans G1 (collecte de toutes les régions de la jeune génération, plus certaines régions de l'ancienne génération avec des rendements de collecte élevés basés sur des statistiques globales de marquage simultané) utilise le marquage simultané, et le marquage simultané introduit deux bitmaps PrevBitMap et NextBitMap, utilisez ces deux bitmaps pour aider à marquer simultanément l'état d'utilisation de la mémoire des différentes étapes.

Afficher les détails 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
......            

Nous pouvons constater que lorsque la JVM initialise le tas de tas (cette fois est le tas G1CollectedHeap utilisé par le collecteur G1), elle créera non seulement un jeu de mémoire, mais aura également une opération create_aux_memory_mapper, qui est utilisée pour les structures de données auxiliaires GC ( tels que : table de cartes, bitmap précédent, bitmap suivant, etc.) pour créer la carte mémoire correspondante. Pour les opérations connexes, vous pouvez afficher le code source de la partie d'initialisation de 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);
  ......
}

Parce que ces structures auxiliaires sont une idée d'espace-temps, elles occuperont inévitablement de la mémoire supplémentaire, en particulier la structure RSet de G1. Lorsque nous augmentons notre mémoire de tas, la mémoire utilisée par GC sera inévitablement également utilisée. La croissance suivante de :

# -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)

Nous pouvons voir que cette surcharge de mémoire supplémentaire est généralement comprise entre 1 % et 20 %, bien sûr si nous n'utilisons pas le collecteur G1, cette surcharge n'est pas si importante :

# -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)

On peut voir qu'en utilisant le UseSerialGC le plus léger, la mémoire occupée par la partie GC est significativement réduite (436M -> 26.67M); en utilisant CMS, la partie GC est réduite de 436M à 163.39M.

GC Cette mémoire est nécessaire, et nous ne pouvons pas la compresser pendant l'utilisation. La pause, le débit et l'utilisation de la mémoire sont le paradoxe ternaire qui ne peut pas être atteint en même temps dans GC. Différents ramasse-miettes ont un accent différent sur les trois. Nous devons examiner et choisir le ramasse-miettes approprié en fonction de nos propres conditions commerciales. .

En raison de l'espace limité, nous continuerons à partager d'autres types de mémoire dans la zone de suivi (y compris Compiler, Internal, Symbol, Native Memory Tracking, Arena Chunk et Unknown) et la mémoire que NMT ne peut pas suivre dans le prochain article, alors restez à l'écoute !

faire référence à

  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

Bienvenue à rejoindre le groupe d'échange Compiler SIG pour échanger et en apprendre davantage sur la technologie de compilation avec tout le monde. Scannez le code pour ajouter un petit assistant WeChat pour vous inviter au groupe d'échange Compiler SIG.

 

Cliquez sur Suivre pour en savoir plus sur les nouvelles technologies de HUAWEI CLOUD pour la première fois ~

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4526289/blog/5586031