Skip to content

Latest commit

 

History

History
264 lines (190 loc) · 15.5 KB

off-heap-memory.adoc

File metadata and controls

264 lines (190 loc) · 15.5 KB

Sautez dans la mémoire off-heap !

Un peu d’histoire…​

A la base, la gestion automatique de la mémoire (nettoyage de la heap) par le Garbage Collector est une des raisons du succès de Java.
Cela a résolu la majorité des bugs que l’on rencontrait historiquement en C et C++, où l’on ne peut pas vérifier que le pointeur utilisé est associé à une zone de mémoire réservée.

De son côté, le principe de mémoire off-heap a été introduit par Java NIO, en Java 1.4 (avec les direct byte buffer, via la classe java.nio.ByteBuffer et sa méthode allocateDirect). Après la création du compilateur JIT en Java 1.2, il s’agissait de continuer à améliorer les performances du langage, qui était sa grande critique du langage à ses débuts.

Modèle mémoire en Java

  • Heap

    • Eden Pool

    • Survivor Pool

    • Tenured Pool

  • Non Heap

    • Method Area
      This area was previously known as the permanent generation where loaded classes were stored. It has recently been removed from the JVM, and classes are now loaded as metadata to native memory of the underlying OS.

    • Native Area
      This area holds references and variables of primitive types.

Voir l’article de Ippon pour le schéma de la mémoire et les explications associées (attention ! article de 2011) Voir également http://stackoverflow.com/questions/2129044/java-heap-terminology-young-old-and-permanent-generations (OU http://stackoverflow.com/a/31103100/1809195 pour avoir directement le commentaire)

Ajouter screenshot de l’onglet Memory de la JConsole ? >> avec la metaspace (ou class metadata space) qui remplace la permanent generation

Expliquer pour la Non Heap (Off Heap) le principe de Thread stack (voir Jenkov)

Pourquoi utiliser la mémoire Off-Heap ?

>> mémoire off-heap = native memory = direct memory (TODO A VERIFIER)

Permet d’outrepasser certaines contraintes inhérentes à Java :

  • pas fait pour gérer de très fortes volumétries de données

  • le GC n’est pas réellement fait pour gérer des objets
    Pour rappel les fonctions du GC sont :

    • de détecter les objets qui ne sont plus référencés (utilisés), et de les effacer

    • de déplacer les survivants d’une zone mémoire à l’autre, afin de libérer de la place pour l’affectation des nouveaux objets Cela convient bien à des objets dont le cycle de vie est court, et pour des JVM de l’ordre de quelques Go.

Par contre, ce principe ne semble pas adapté à des objets lourds et à cycle de vie (très) long.
Ces derniers vont en effet être régulièrement déplacés d’une zone mémoire à l’autre, ce qui va être d’autant plus pénalisant que les objets sont lourds, et que l’opération de déplacement du GC est bloquante (VERIFIER DANS QUELS CAS EXACTEMENT)

>> la mémoire off-heap n’est en elle-même ni plus ni moins rapide que la heap, The point of BigMemory is not that native memory is faster, but rather, it’s to reduce the overhead of the garbage collector having to go through the effort of tracking down references to memory and cleaning it up. TODO voir http://stackoverflow.com/a/5864736/1809195

Il s’agissait de trouver le meilleur ratio entre taille de la Heap et péjoration des performances dues aux traitements du Garbage Collector. Plus la taille de la Heap est grande, plus longues sont les opérations associées du Garbage Collector.

Une solution : sauvegarder les données dans une zone mémoire non gérée, non vue, par le GC (appelée mémoire Off-Heap)

Allocation de mémoire off heap en Java

  • Tous les objets instanciés à l’aide de new sont alloués dans la Heap.

  • Non Direct ByteBuffer

  • Off Heap

    • utilisation de JNA

    • Direct ByteBuffer

    • MemoryMappedFile

    • sun.misc.Unsafe

Java nio a introduit le principe de mémoire off-heap (java 1.4). Java 7 l’a étendu (JSR-203/NIO2).

Les objets concernés sont les ByteBuffers (java.nio.ByteBuffer) avec allocation directe (maximum de 2 Go par buffer). Concrètement il s’agit d’un tableau de Bytes qui sera stocké dans la mémoire native. La nature même du stockage (sous forme binaire) impose une sérialisation/désérialisation des objets.

SYNTHESE DES 2 parties à faire

Cela passe par l’utilisation de la classe java.nio.ByteBuffer et de sa méthode allocateDirect.

Les objets instanciés à partir de cette classe vont être stockés dans la Heap.
Par contre, il va s’agir de "pointeurs", très légers en eux-mêmes, vers une zone mémoire en dehors de la Heap.

Manipuler le contenu de ces objets n’est pas sans contraintes :

  • ce contenu n’est accessible que par le principe de sérialisation / désérialisation.
    En effet, ce que Java va stocker dans la mémoire off-heap va être considéré comme des octets (bytes), et non des objets.
    Ceci n’est évidemment pas bon en termes de performance.

>> Direct memory can be faster than using a byte[] if you use use non bytes like int as it can read/write the whole four bytes without turning the data into bytes. However it is slower than using POJOs as it has to bounds check every access.

Par contre, c’est très performant pour les type primitifs. On ne met QUE des types primitifs en Off-Heap. Jenkov: All local variables of primitive types ( boolean, byte, short, char, int, long, float, double) are fully stored on the thread stack and are thus not visible to other threads.

De plus, la perte du pointeur vers le buffer rend ce dernier éligible à la garbage collection. La mémoire associée au pointeur est libérée au moment de la collecte.

>> il n’y a pas de méthode pour désallouer un objet stocké hors heap. En réalité une méthode de libération de la mémoire est crée automatiquement (sun.misc.Cleaner) et sera appelée par le GC lors de son prochain passage. sun.misc.Cleaner, se renseigner !

//Objet léger qui pointe vers la mémoire.
ByteBuffer bb = ByteBuffer.allocateDirect(1024);
bb.putInt(15);
bb.putChar('a');
bb.rewind();
int myInt = bb.getInt();
char myChar = bb.getChar();

libération de la mémoire (ByteBuffer)

Tout comme pour la heap, l’espace est libéré par le GC lorsque l’objet n’est plus référencé par le code. Tout comme la heap il n’y a pas de relation directe entre le moment ou l’objet est libérable et le moment ou il est effectivement libéré. Donc il n’y a pas de magie, les objets hors heap sont bien sensibles au GC.

Toutefois :

Pas de phase de marquage des objets. Pas de phase de compaction (réorganisation de l’espace mémoire) pendant le passage du GC. Le nettoyage de la mémoire hors heap est donc plus rapide que son homologue de la heap. Il est possible d’appeler la méthode de nettoyage à tout moment (encore une fois en fouillant dans les profondeurs de l’API) :

Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner)getCleanerMethod.invoke(buffer, new Object[0]); cleaner.clean();

DirectByteBuffer : il y a un overhead, du fait de certaines opérations supplémentaires, comme la détection de l’architecture petit-boutiste (little-endian), ou gros-boutiste de (big-endian) de l’OS sous-jacent. Pour la solution ActivePivot, la classe (DirectByteBuffer) a été réimplémentée afin de ne pas effectuer ces opérations supplémentaires. Cette réimplémentaion nécessite l’utilisation de la classe Unsafe

Options de la JVM associées à la mémoire Off-Head

-XX:MaxDirectMemorySize= ou -Dsun.nio.MaxDirectMemorySize= Permet de définir la mémoire maximale réservées pour la mémoire off heap.

sun.misc.Unsafe

TODO : actualité, parler de la levée de boucliers devant la possible suppression de Unsafe en Java 9

Cette classe permet de manipuler directement la mémoire en Java. Elle est utilisée par ByteBuffer.allocateDirect().

A la base, elle n’est pas censé être utilisée en dehors du jdk. Son accès est protégé, et il faut donc se servir de l’introspection pour pouvoir l’utiliser. >> les constructeurs sont privés et la méthode de classe getUnsafe() ne peut être appelée que par un Bootloader (et donc par la JVM elle même). >> TODO : l’histoire du Bootloader est à préciser

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

ecriture

Avec Unsafe, nous pouvons allouer de la mémoire à un emplacement dont on obtient l’adresse :

long address = unsafe.allocatememory(1024);

A partir de là, il est possible d’y insérer des données :

unsafe.putInt(address, 10);

en prenant soin de gérer manuellement leur position en mémoire

unsafe.putChar(address + 4, 'x')

Ici nous avions insérer dans un 1er temps un int, donc 4 octets, donc il faut tenir compte lors de l’ajout du char suivant.

autre example :

// Récupère une instance Unsafe
Unsafe unsafe = getUnsafeInstance();
// Réserve de la mémoire directe
Long allocateMemory = unsafe.allocateMemory(10);
// Récupération de l'espace d'allocation du champs code Commune
Field field = Commune.class.getDeclaredField("codeCommune");
Long offsetCodeCommune = unsafe.objectFieldOffset(field);
// On affecte une valeur à l'emplacement du champ
unsafe.putObject(allocateMemory, offsetCodeCommune, "325555");

lecture

Le même raisonnement s’applique pour la lecture des données

Les risques

PRECISER : crash suite à mauvais accès mémoire Si on essaye d’écrire dans une zone non allouée. >> l’accès à une zone mémoire non allouée provoque immanquablement le crash de la JVM.

l’utilisation de Unsafe nécessite une vérification à chaque montée de version de Java.

Cas d’utilisation de la mémoire Off-Heap

  • ActivePivot : base de données en mémoire, écrite en Java, très grosse volumétrie, très fortes contraintes de performance

  • memory mapped file

  • OpenHFT (HigherFrequencyTrading) / Chronicle (nouveau nom / http://chronicle.software/) : Peter Lawrey is Lead Developper

  • Redis (REmote Dictionary Server, used by StackOverFlow, GitHub, Twitter)

Resources