1. Préface
Java est un langage de programmation de haut niveau orienté objet de haut niveau. Les programmes Java fonctionnent sur Java Virtual Machines (JVMS) et gérent la mémoire par JVMS. C'est la plus grande différence par rapport à C ++. Bien que la mémoire soit gérée par JVMS, nous devons également comprendre comment JVM gère la mémoire. Il n'y a pas seulement un JVM, et il peut y avoir des dizaines de machines virtuelles qui existent actuellement, mais une conception de machines virtuelles conforme à la spécification doit suivre la "spécification de la machine virtuelle Java". Cet article est basé sur la description de Hotspot Virtual Machine et sera mentionné s'il existe des différences avec d'autres machines virtuelles. Cet article décrit principalement comment la mémoire est distribuée dans JVM, comment les objets du programme Java sont stockés et accessibles, et des exceptions possibles dans divers domaines de mémoire.
2. Distribution de la mémoire (région) dans JVM
Lors de l'exécution de programmes Java, le JVM divise la mémoire en plusieurs zones de données différentes pour la gestion. Ces domaines ont des fonctions, des temps de création et de destruction différentes. Certains domaines sont alloués lorsque le processus JVM est démarré, tandis que d'autres sont liés au cycle de vie du thread utilisateur (le fil du programme lui-même). Selon la spécification JVM, les zones de mémoire gérées par le JVM sont divisées en zones de données d'exécution suivantes:
1. pile de machines virtuelles
Cette zone de mémoire est privée par le fil et est créée lorsque le fil démarre et détruit lorsqu'il est détruit. Le modèle de mémoire pour l'exécution des méthodes Java décrits par la pile de machines virtuelles: chaque méthode créera une trame de pile (trame de pile) au début de l'exécution, qui est utilisée pour stocker des tables variables locales, des piles d'opérande, des liens dynamiques, des sorties de la méthode, etc. L'exécution et le retour de chaque méthode sont terminés, et il existe un cadre de pile sur la pile de la machine virtuelle.
Comme son nom l'indique, le tableau des variables locales est un domaine de mémoire qui stocke les variables locales: il stocke les types de données de base (8 types de données de base Java), les types de référence et les adresses de retour qui peuvent être trouvées pendant la période du compilateur; Les types longs et doubles qui occupent 64 bits occuperont 2 espaces variables locaux, et d'autres types de données occupent uniquement 1; Étant donné que la taille du type est déterminée et que le nombre de variables peut être connu pendant la période de compilation, la table variable locale a une taille connue lorsqu'elle est créée. Cette partie de l'espace mémoire peut être allouée pendant la période de compilation, et il n'est pas nécessaire de modifier la taille de la table variable locale pendant l'exécution de la méthode.
Dans la spécification de la machine virtuelle, deux exceptions sont spécifiées pour cette zone de mémoire:
1. Si la profondeur de pile demandée par le thread est supérieure à la profondeur autorisée (?), StackOverflowError sera lancée;
2. Si la machine virtuelle peut se développer dynamiquement, lorsque l'expansion ne peut pas demander une mémoire suffisante, une exception OutOfMemory sera lancée;
2. pile de méthode locale
La pile de méthodes locale est également du thread-privé, et sa fonction est presque la même que la pile de machines virtuelles: la pile de machine virtuelle fournit des services de pile dans et out pour l'exécution de la méthode Java, tandis que la pile de méthode locale fournit des services pour que la machine virtuelle exécute des méthodes natives.
Dans la spécification de la machine virtuelle, il n'y a pas de réglementation obligatoire sur la méthode d'implémentation de la pile de méthode locale, et il peut être implémenté librement par la machine virtuelle spécifique; La machine virtuelle Hotspot combine directement la pile de machine virtuelle et la pile de méthode locale en une seule; Pour que d'autres machines virtuelles implémentent cette méthode, les lecteurs peuvent interroger les informations pertinentes si elles sont intéressées;
Comme la pile de machines virtuelles, la pile de méthode locale lancera également StackOverflowError和OutOfMemory Exceptions.
3. Calculateur de programme
Le calculateur de programme est également une zone de mémoire privée des threads. Il peut être considéré comme un indicateur de numéro de ligne (pointant vers une instruction) pour que les threads exécutent des bytecode. Lorsque Java est exécuté, il obtient l'instruction suivante à exécuter en modifiant la valeur du compteur. Les ordres d'exécution des succursales, boucles, sauts, manipulation des exceptions, récupération de thread, etc. s'appuient tous sur ce compteur pour terminer. Le multithreading d'une machine virtuelle est réalisé en changeant à son tour et en allouant le temps d'exécution du processeur. Le processeur (un noyau pour un processeur multi-core) ne peut exécuter qu'une seule commande à la fois. Par conséquent, une fois que le thread a effectué la commutation, il doit être restauré à la position d'exécution correcte. Chaque thread a une calculatrice de programme indépendante.
Lors de l'exécution d'une méthode Java, la calculatrice du programme enregistre (pointe vers) l'adresse de l'instruction bytecode que le thread actuel exécute. Si la méthode native est en cours d'exécution, la valeur de cette calculatrice n'est pas définie. En effet, le modèle de thread de machine virtuelle Hotspot est un modèle de thread natif, c'est-à-dire que chaque thread Java mappe directement le thread du système d'exploitation (système d'exploitation). Lors de l'exécution de la méthode native, il est directement exécuté par le système d'exploitation. La valeur de ce compteur de la machine virtuelle est inutile; Étant donné que cette calculatrice est une zone de mémoire avec un très petit espace, privé et ne nécessite pas d'expansion. C'est le seul domaine de la spécification de la machine virtuelle qui ne spécifie aucune exception OutOfMemoryError .
4. Mémoire de tas (tas)
Le tas Java est une zone de mémoire partagée par les threads. On peut dire qu'il s'agit de la plus grande zone de mémoire gérée par la machine virtuelle et est créée lorsque la machine virtuelle est démarrée. La mémoire de tas Java stocke principalement les instances d'objets, et presque toutes les instances d'objet (y compris les tableaux) sont stockées ici. Par conséquent, il s'agit également de la principale zone de mémoire de la collecte des ordures (GC). Le contenu sur GC ne sera pas décrit ici;
Selon la spécification de la machine virtuelle, la mémoire de tas Java peut être en mémoire physique discontinue. Tant qu'il est logiquement continu et qu'il n'y a pas de limite à l'expansion de l'espace, il peut être une taille fixe ou un arbre étendu. Si la mémoire du tas n'a pas assez d'espace pour terminer l'allocation d'instance et ne peut pas être élargie, une exception OutOfMemoryError sera lancée.
5. Zone de méthode
La zone de méthode est la zone de mémoire partagée par les threads, tout comme la mémoire du tas, il stocke les informations de type, les constantes, les variables statiques, le code compilé pendant la période de compilation instantanée et d'autres données. La spécification de la machine virtuelle n'a pas trop de restrictions sur la mise en œuvre de la zone de méthode, et comme la mémoire du tas, elle ne nécessite pas d'espace mémoire physique continu, la taille peut être fixe ou évolutive, et il peut également être choisi pour ne pas implémenter la collecte des ordures; Lorsque la zone de méthode ne peut pas répondre aux exigences d'allocation de mémoire, l'exception OutOfMemoryError sera lancée.
6. Mémoire directe
La mémoire directe ne fait pas partie de la mémoire gérée de la machine virtuelle, mais cette partie de la mémoire peut toujours être utilisée fréquemment; Lorsque les programmes Java utilisent des méthodes natives (telles que NIO, NIO, aucune descriptions n'est donnée ici), la mémoire peut être allouée directement hors trémail, mais l'espace mémoire total est limité, et il y aura une mémoire insuffisante, et une exception OutOfMemoryError sera également jetée.
2. Accès du stockage d'objets d'instance
Le premier point ci-dessus a une description générale de la mémoire dans chaque zone de la machine virtuelle. Pour chaque domaine, il y a des problèmes avec la façon dont les données sont créées, présentées et accédés. Utilisons la mémoire de tas de tas la plus couramment utilisée comme exemple pour parler de ces trois aspects basés sur le hotspot.
1. Création d'objets d'instance
Lorsque la machine virtuelle exécute une nouvelle instruction, elle localise d'abord la référence de symbole de classe de l'objet de création à partir du pool constant et juge si la classe a été chargée et initialisée. S'il n'est pas chargé, le processus d'initialisation de charge de classe sera exécuté (la description ne sera pas effectuée ici sur le chargement des classes). Si cette classe ne peut être trouvée, une exception de classe ClassNotFoundException sera lancée;
Après la vérification du chargement des cours, la mémoire physique (mémoire du tas) est en fait allouée à l'objet. L'espace mémoire requis par l'objet est déterminé par la classe correspondante. Après le chargement de la classe, l'espace mémoire requis par l'objet de cette classe est fixe; L'allocation de l'espace mémoire pour l'objet équivaut à diviser une pièce du tas et à l'allocation à cet objet;
Selon si l'espace mémoire est continu (alloué et non alloué est divisé en deux parties complètes), il est divisé en deux façons d'allouer la mémoire:
1. Mémoire continue: un pointeur est utilisé comme point de division entre la mémoire allouée et non allouée. L'allocation de mémoire d'objet nécessite uniquement que le pointeur déplace la taille de l'espace vers le segment de mémoire non alloué; Cette méthode est appelée "collision de pointeur".
2. Mémoire discontinue: la machine virtuelle doit maintenir (enregistrer) une liste qui enregistre ces blocs de mémoire dans le tas qui ne sont pas alloués. Lorsque vous allouez la mémoire de l'objet, sélectionnez une zone de mémoire de taille appropriée pour l'allouer à l'objet et mettre à jour cette liste; Cette méthode est appelée "liste gratuite".
L'allocation de la mémoire d'objet rencontrera également des problèmes de concurrence. La machine virtuelle utilise deux solutions pour résoudre ce problème de sécurité de thread: d'abord, utilisez CAS (comparer et set) + pour identifier et réessayer pour assurer l'atomicité de l'opération d'allocation; Deuxièmement, l'allocation de la mémoire est divisée en différents espaces en fonction des threads, c'est-à-dire que chaque thread a pré-alloué un morceau de mémoire de thread-privé dans le tas, appelé tampon local alloué au thread (TLAB); Lorsque ce fil veut allouer la mémoire, il est directement alloué à partir du TLAB. Ce n'est que lorsque le TLAB du fil est alloué après une réallocation que l'opération synchrone peut être allouée à partir du tas. Cette solution réduit efficacement la concurrence de la mémoire du tas d'allocation d'objets entre les threads; Si la machine virtuelle utilise TLAB est définie via le paramètre JVM -XX: +/- USETLAB.
Après avoir terminé l'allocation de mémoire, en plus des informations d'en-tête d'objet, la machine virtuelle initialise l'espace mémoire alloué à la valeur nulle pour s'assurer que les champs de l'instance d'objet peuvent être directement utilisés à la valeur zéro correspondant au type de données sans attribuer de valeurs; Ensuite, exécutez la méthode init pour terminer l'initialisation en fonction du code avant la fin de la création d'un objet d'instance;
2. La disposition des objets en mémoire
Dans la machine virtuelle Hotspot, les objets sont divisés en trois parties en mémoire: en-tête d'objet, données d'instance et alignement et remplissage:
L'en-tête d'objet est divisé en deux parties: une partie de l'informatique stocke les données d'exécution de l'objet, y compris le code de hachage, l'âge de génération de collecte des ordures, l'état de verrouillage de l'objet, le verrouillage de maintien du fil, l'ID de fil biaisé, l'horodatage biaisé, etc.; Dans les machines virtuelles 32 bits et 64 bits, cette partie des données occupe respectivement 32 bits et 64 bits; Puisqu'il y a beaucoup de données d'exécution, 32 bits ou 64 bits ne sont pas suffisants pour stocker complètement toutes les données, de sorte que cette pièce est conçue pour stocker les données d'exécution dans un format non fixe, mais utilise différents bits pour stocker des données en fonction de l'état de l'objet; L'autre partie stocke le pointeur de type d'objet, pointant vers la classe de cet objet, mais ce n'est pas nécessaire, et les métadonnées de classe de l'objet n'ont pas nécessairement besoin d'être déterminées en utilisant cette partie du stockage (elle sera discutée ci-dessous);
Les données d'instance sont le contenu de divers types de données définies par l'objet, et les données définies par ces programmes ne sont pas stockées dans l'ordre défini. Ils sont déterminés dans l'ordre des politiques et définitions d'allocation de machines virtuelles: long / double, int, court / char, octet / booléen, OOP (objet ordinaire ponint) , on peut voir que les politiques sont allouées en fonction du nombre d'espaces réservés du type, et les mêmes types allouent la mémoire ensemble; et, sous la satisfaction de ces conditions, l'ordre des variables de classe parent est précédé par la sous-classe;
La partie de remplissage d'objet n'existe pas nécessairement. Il ne joue qu'un rôle dans l'alignement d'espace réservé. Dans le hotspot virtuel, la gestion de la mémoire de la machine est gérée en unités de 8 octets. Par conséquent, lorsque la mémoire est allouée, la taille de l'objet n'est pas un multiple de 8 et le remplissage d'alignement est terminé;
3. Accès d'objet <br /> Dans le programme Java, nous créons un objet, et en fait nous obtenons une variable de type de référence, à travers laquelle nous opérons réellement une instance dans la mémoire du tas; Dans la spécification de la machine virtuelle, il est stipulé que le type de référence est une référence pointant vers l'objet, et il ne spécifie pas comment cette référence localise et accède aux instances du tas; Actuellement, dans les machines virtuelles grand public, il existe deux façons principales d'implémenter l'accès à l'objet:
1. Méthode de poignée: Une région est divisée en mémoire de tas comme pool de poignées. La variable de référence stocke l'adresse de poignée de l'objet et la poignée stocke les informations d'adresse spécifique de l'échantillon d'objet et de type d'objet. Par conséquent, l'en-tête d'objet ne peut pas contenir le type d'objet:
2. Accès direct au pointeur: Le type de référence stocke directement les informations d'adresse de l'objet d'instance dans le tas, mais cela nécessite que la disposition de l'objet d'instance doit contenir le type d'objet:
Ces deux méthodes d'accès présentent leurs propres avantages: lorsque l'adresse de l'objet est modifiée (tri de mémoire, collecte des ordures), l'objet d'accès à la poignée, la variable de référence n'a pas besoin d'être modifiée, mais seule la valeur d'adresse de l'objet dans la poignée est modifiée; Tout en utilisant la méthode d'accès direct au pointeur, toutes les références de cet objet doivent être modifiées; Mais la méthode du pointeur peut réduire une opération d'adressage, et dans le cas d'un grand nombre d'accès d'objets, les avantages de cette méthode sont plus évidents; La machine virtuelle Hotspot utilise cette méthode d'accès direct au pointeur.
3. Exception de mémoire d'exécution
Il y a deux exceptions principales qui peuvent se produire lors de l'exécution dans le programme Java: OutOfMemoryError et StackOverflowerror; Que se passera-t-il dans cette zone de mémoire? Comme mentionné brièvement précédemment, à l'exception du compteur du programme, d'autres domaines de mémoire se produiront; Cette section démontre principalement les exceptions dans chaque zone de mémoire via le code d'instance, et de nombreux paramètres de démarrage virtuel couramment utilisés seront utilisés pour mieux expliquer la situation. (Comment exécuter le programme avec des paramètres n'est pas décrit ici)
1. Java Heat Memory Overflow
Le débordement de la mémoire du tas se produit lorsque les objets sont créés après que la capacité du tas atteigne la capacité de tas maximale. Dans le programme, les objets sont créés en continu et ces objets sont garantis pour ne pas être collectés:
/ ** * Paramètres de la machine virtuelle: * -xms20m Capacité de tas minimum * -xmx20m Capacité de tas maximale * @Author hwz * * / public class hakoutofMemoryError {public static void main (String [] args) {// Utiliser un conteneur ArrayList <HeadoutofMemoryError> (); while (true) {// Créez en continu des objets et ajoutez-les au conteneur listoholdobj.add (nouveau HeadOfMemoryError ()); }}} Vous pouvez ajouter des paramètres de machine virtuelle :-XX:HeapDumpOnOutOfMemoryError . Lors de l'envoi d'une exception OOM, laissez la machine virtuelle vider le fichier instantané du tas actuel. Vous pouvez utiliser ce problème d'exception de segmentation des mots de fichier à l'avenir. Cela ne sera pas décrit en détail. J'écrirai un blog pour décrire en détail à l'aide de l'outil MAT pour analyser les problèmes de mémoire.
2. Pile de machine virtuelle et débordement de pile de méthode locale
Dans la machine virtuelle Hotspot, ces deux piles de méthodes ne sont pas implémentées ensemble. Selon la spécification de la machine virtuelle, ces deux exceptions se produiront dans ces deux zones de mémoire:
1. Si le thread demande à la profondeur de la pile supérieure à la profondeur maximale autorisée par la machine virtuelle, lancez une exception Stackoverflowerror;
2. Si la machine virtuelle ne peut pas demander un grand espace mémoire lors de l'expansion de l'espace de pile, une exception OutOfMemoryError sera lancée;
Il y a en fait un chevauchement entre ces deux situations: lorsque l'espace de pile ne peut pas être alloué, il est impossible de distinguer si la mémoire est trop petite ou que la profondeur de pile utilisée est trop grande.
Utilisez deux façons de tester le code
1. Utilisez le paramètre -xss pour réduire la taille de la pile, appelez une méthode infiniment récursivement et augmentez la profondeur de la pile à l'infini:
/ ** * Paramètres de machine virtuelle: <br> * -xss128k Capacité de pile * @author hwz * * / classe publique stackOverflowerRor {private int stackdeep = 1; / ** * Recursion infinie, agrandir infiniment la profondeur de la pile d'appels * / public void récursiveInvoke () {stackDep ++; RecursiveInvoke (); } public static void main (String [] args) {stackOverflowerRor soe = new StackOverflowerRor (); essayez {soe.recursiveInvoke (); } catch (Throwable E) {System.out.println ("Stack Deep =" + soe.stackdeep); jeter e; }}} Un grand nombre de variables locales sont définies dans la méthode, la longueur de la table variable locale dans la pile de méthode est également appelée infiniment récursive:
/ ** * @author hwz * * / classe publique StackOomeError {private int stackdeep = 1; / ** * Définissez un grand nombre de variables locales, augmentez la table variable locale dans la pile * Recursion infinie, augmentez infiniment la profondeur de la pile d'appel * / public void récursiveInvoke () {double i; Double I2; //........ Le grand nombre de définitions de variables est omise ici stackdeep ++; RecursiveInvoke (); } public static void main (String [] args) {stackoomeError soe = new StackoomeError (); essayez {soe.recursiveInvoke (); } catch (Throwable E) {System.out.println ("Stack Deep =" + soe.stackdeep); jeter e; }}}Le test de code ci-dessus montre que peu importe si la pile de trame est trop grande ou que la capacité de machine virtuelle est trop petite, lorsque la mémoire ne peut pas être allouée, tout Stackoverflowerror est lancé;
3. Méthode Area et Runtime Constante Pool Overflow
Ici, nous décrire d'abord la méthode interne de la chaîne: si le pool de constante de chaîne contient déjà une chaîne égale à cet objet de chaîne, il renverra un objet de chaîne représentant cette chaîne. Sinon, ajoutez cet objet String au pool constant et renvoyez une référence à cet objet String; Grâce à cette méthode, il ajoutera en continu un objet de chaîne au pool constant, entraînant un débordement:
/ ** * Paramètres de la machine virtuelle: <br> * -xx: permsize = 10m taille permanente de la zone * -xx: maxpermSize = 10m zone permanente maximum capacité * @author hwz * * / public class runtimeConStancePooloom {public static void main (String [] args) {// Utiliser un container pour enregistrer l'objet pour garantir ArrayList <string> (); // Utilisez la méthode String.intern pour ajouter l'objet du pool constant pour (int i = 1; true; i ++) {list.add (string.valueof (i) .intern ()); }}}Cependant, ce code de test ne déborde pas pendant le pool constant d'exécution dans JDK1.7, mais il se produira dans JDK1.6. Pour cette raison, écrivez un autre code de test pour vérifier ce problème:
/ ** * string.intern La méthode est testée sous différents jdks * @author hwz * * / public class stringInternTest {public static void main (string [] args) {String str1 = new StringBuilder ("test"). APPEND ("01"). ToString (); System.out.println (str1.intern () == str1); String str2 = new StringBuilder ("test"). APPEND ("02"). TOSTRING (); System.out.println (str2.intern () == str2); }} Les résultats de l'exécution sous JDK1.6 sont: faux, faux;
Le résultat de l'exécution sous JDK1.7 est: vrai, vrai;
Il s'avère que dans JDK1.6, la méthode inter () copie la première instance de chaîne rencontrée à la génération permanente, qui à son tour est une référence à l'instance de la génération permanente, et les instances de chaîne créées par StringBuilder sont dans le tas, donc elles ne sont pas égales;
Dans JDK1.7, la méthode inter () ne copie pas l'instance, mais enregistre uniquement la référence de la première instance qui apparaît dans le pool constant. Par conséquent, la référence renvoyée par Intern est la même que l'instance créée par StringBuilder, donc elle renvoie True;
Par conséquent, le code de test pour le débordement de pool constant n'aura pas d'exception de débordement de pool constant, mais peut avoir une exception de débordement de mémoire de tas insuffisante après l'exécution continue;
Ensuite, vous devez tester le débordement de la zone de la méthode, continuez simplement d'ajouter des choses à la zone de méthode, telles que les noms de classe, les modificateurs d'accès, les pools constants, etc. Nous pouvons laisser le programme charger un grand nombre de classes pour remplir en continu la zone de méthode, ce qui conduit à un débordement. Nous utilisons CGLIB pour manipuler directement le bytecode pour générer un grand nombre de classes dynamiques:
/ ** * Méthode Area Memory Overflow Test Class * @author hwz * * / public class MethodAreaoom {public static void main (String [] args) {// Utilisez gclib pour créer des sous-classes infiniment while (true) {Enhancer Enhancer = new Enhancer (); Enhancer.SetSuperclass (maoomclass.class); Enhancer.SetUseCache (false); Enhancer.SetCallback (new MethodInterceptor () {@Override public Object Intercept (objet obj, méthode méthode, objet [] args, méthodyproxy proxy) lève le throws {return proxy.invokeuper (obj, args);}}); Enhancer.Create (); }} classe statique maoomclass {}} Grâce à l'observation VisualVM, nous pouvons voir que le nombre de classes chargées JVM augmente en ligne droite avec l'utilisation de Pergen:
4. débordement de mémoire directe
La taille de la mémoire directe peut être définie à travers les paramètres de la machine virtuelle : -xx: maxDirectMemorySize . Pour faire un débordement de mémoire directe, il vous suffit de postuler en continu pour la mémoire directe. Ce qui suit est le même que le test de cache de mémoire directe dans Java Nio:
/ ** * Paramètres de la machine virtuelle: <br> * -xx: maxDirectMemorySize = 30m Taille directe de la mémoire * @Author HWZ * * / classe publique DirectMemoryoom {public static void main (String [] args) {list <buffer> buffrs = new ArrayList <Buffer> (); int i = 0; while (true) {// imprime le système actuel.out.println (++ i); // Consommation directe de la mémoire en appliquant en continu la consommation directe de mémoire de tampon dans le tampon de cache.add (bytebuffer.AllocateIrect (1024 * 1024)); // comptabilité 1m à chaque fois}}} Dans la boucle, chaque fois que 1 m de mémoire directe est appliquée, la mémoire directe maximale est définie sur 30 m et une exception est lancée lorsque le programme s'exécute 31 fois: java.lang.OutOfMemoryError: Direct buffer memory
4. Résumé
Ce qui précède est tout le contenu de cet article. Cet article décrit principalement la structure de mise en page de la mémoire, du stockage d'objets et des exceptions de mémoire qui peuvent se produire dans diverses zones de mémoire du JVM; Le livre de référence principal "Compréhension approfondie de la machine virtuelle Java (deuxième édition)". S'il y a une incorrecte, veuillez le signaler dans les commentaires; Merci pour votre soutien à Wulin.com.