OpenJDK JVM va-t-il rendre la mémoire de tas à Linux?

Nous avons un processus de serveur de longue durée, qui nécessite peu de temps de mémoire vive. Nous constatons qu’une fois que la JVM a récupéré la mémoire du système d’exploitation, elle ne la renvoie jamais au système d’exploitation. Comment pouvons-nous demander à la JVM de restaurer la mémoire de tas sur le système d’exploitation?

En règle générale, la réponse à ces questions est d’utiliser -XX:MaxHeapFreeRatio et -XX:MinHeapFreeRatio . (Voir par exemple 1 , 2 , 3 , 4 ). Mais nous courons Java comme ceci:

 java -Xmx4G -XX:MaxHeapFreeRatio=50 -XX:MinHeapFreeRatio=30 MemoryUsage 

et encore voir dans VisualVM:

Utilisation de la mémoire Visual VM

Clairement, la JVM ne respecte pas -XX:MaxHeapFreeRatio=50 car le heapFreeRatio est très proche de 100% et loin de 50%. Si vous ne cliquez pas sur “Effectuer GC”, la mémoire retourne dans le système d’exploitation.

MemoryUsage.java:

 import java.util.ArrayList; import java.util.List; public class MemoryUsage { public static void main(Ssortingng[] args) throws InterruptedException { System.out.println("Sleeping before allocating memory"); Thread.sleep(10*1000); System.out.println("Allocating/growing memory"); List list = new ArrayList(); // Experimentally determined factor. This gives approximately 1750 MB // memory in our installation. long realGrowN = 166608000; // for (int i = 0 ; i < realGrowN ; i++) { list.add(23L); } System.out.println("Memory allocated/grown - sleeping before Garbage collecting"); Thread.sleep(10*1000); list = null; System.gc(); System.out.println("Garbage collected - sleeping forever"); while (true) { Thread.sleep(1*1000); } } } 

Versions:

 > java -version openjdk version "1.8.0_66-internal" OpenJDK Runtime Environment (build 1.8.0_66-internal-b01) OpenJDK 64-Bit Server VM (build 25.66-b01, mixed mode) > uname -a Linux londo 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u5 (2015-10-09) x86_64 GNU/Linux > lsb_release -a No LSB modules are available. Dissortingbutor ID: Debian Description: Debian GNU/Linux 8.2 (jessie) Release: 8.2 Codename: jessie 

J’ai également essayé OpenJDK 1.7 et Sun Java 1.8. Tous se comportent de la même manière et aucun ne renvoie de mémoire à l’OS.

Je pense que j’en ai besoin et que l’échange et la pagination ne vont pas “résoudre” cela, car dépenser des données d’E / S sur la pagination de près de 2 Go de données n’est qu’un gaspillage de ressources. Si vous n’êtes pas d’accord, veuillez m’éclairer.

J’ai également écrit un peu memoryUsage.c avec malloc() / free() , et il renvoie la mémoire au système d’exploitation. Donc c’est possible en C. Peut-être pas avec Java?

Edit: Augusto a souligné que la recherche m’aurait conduit à -XX:MaxHeapFreeRatio et -XX:MinHeapFreeRatio ne fonctionnait qu’avec -XX:+UseSerialGC . J’étais en extase et j’ai essayé, perplexe de ne pas l’avoir trouvé moi-même. Oui, cela a fonctionné avec mon MemoryUsage.java:

-XX: + UseSerialGC fonctionne avec une application simple

Cependant, quand j’ai essayé -XX:+UseSerialGC avec notre vraie application, pas tellement:

-XX: + UseSerialGC ne fonctionne pas avec une application réelle

J’ai découvert que gc () après un certain temps a aidé, donc j’ai fait un thread qui a fait plus ou moins:

 while (idle() && memoryTooLarge() && ! tooManyAttemptsYet()) { Thread.sleep(10*1000); System.gc(); } 

et ça a fait l’affaire:

GC thread travaillant

J’avais en fait déjà vu le comportement avec -XX:+UseSerialGC et plusieurs appels -XX:+UseSerialGC System.gc() dans certaines de mes nombreuses expériences, mais je n’aimais pas la nécessité d’un thread GC. Et qui sait si cela va continuer à fonctionner tant que notre application et Java évoluent. Il doit y avoir un meilleur moyen.

Quelle est la logique qui m’oblige à appeler System.gc() quatre fois (mais pas immédiatement), et où est ce document?

À la recherche de la documentation pour -XX:MaxHeapFreeRatio et -XX:MinHeapFreeRatio ne fonctionne qu’avec -XX:+UseSerialGC , j’ai lu la documentation de l’outil / exécutable java et il n’est mentionné nulle part que -XX:MaxHeapFreeRatio et -XX:MinHeapFreeRatio ne fonctionne qu’avec -XX:+UseSerialGC . En fait, le problème résolu [JDK-8028391] Rendre les indicateurs Min / MaxHeapFreeRatio gérables, dit:

Pour permettre aux applications de contrôler quand et comment autoriser plus ou moins de GC, les indicateurs -XX: MinHeapFreeRatio et -XX: MaxHeapFreeRatio doivent être gérables. La prise en charge de ces indicateurs doit également être implémentée dans le collecteur parallèle par défaut.

Un commentaire sur le problème résolu dit:

La prise en charge de ces indicateurs a également été ajoutée à ParallelGC dans le cadre de la stratégie de taille adaptative.

J’ai vérifié, et le patch référencé dans le problème corrigé backported à openjdk-8 est bien contenu dans l’ archive du paquet source pour la version openjdk-8 que j’utilise. Donc, il devrait apparemment fonctionner dans “le collecteur parallèle par défaut”, mais cela n’a pas été le cas dans cet article. Je n’ai pas encore trouvé de documentation indiquant qu’il ne devrait fonctionner qu’avec -XX:+UseSerialGC . Et comme je l’ai documenté ici, même cela n’est pas fiable / risqué.

Je ne peux pas juste obtenir -XX:MaxHeapFreeRatio et -XX:MinHeapFreeRatio pour faire ce qu’ils promettent sans avoir à passer par tous ces obstacles?

    “G1 (-XX: + UseG1GC), nettoyage parallèle (-XX: + UseParallelGC) et ParallelOld (-XX: + UseParallelOldGC) renvoient de la mémoire lorsque le segment de mémoire diminue. Je ne suis pas sûr de Serial et de CMS, ils ne l’ont pas fait.” t réduire leur tas dans mes expériences.

    Les deux collecteurs parallèles nécessitent un certain nombre de GC avant de réduire le tas à une taille “acceptable”. Ceci est par conception. Ils gardent délibérément le tas en supposant que cela sera nécessaire à l’avenir. Définir le drapeau -XX: GCTimeRatio = 1 améliorera quelque peu la situation mais il faudra encore plusieurs GC pour réduire considérablement.

    G1 est remarquablement bon pour réduire le tas rapidement, donc pour le cas d’utilisation décrit ci-dessus, je dirais qu’il peut être résolu en utilisant G1 et en exécutant System.gc() après avoir libéré tous les caches et chargeurs de classes, etc.

    http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6498735

    S’il y a une méthode (disons m1 ()) qui fait le gros du travail dans votre application, vous pouvez essayer ceci:

    1. au lieu d’appeler le m1 (), serialize tous les objects requirejs pour que la méthode fonctionne.
    2. créer une méthode principale qui de-serialize les objects que vous avez sérialisés à l’étape précédente. et appelle m1 () – cette méthode principale est un point d’entrée pour un programme P1.
    3. À la fin de m1 (), sérialisez la sortie du programme principal pour la désérialiser.
    4. exécuter P1 en tant que programme distinct en utilisant Runtime or ProcessBuilder .

    De cette façon, lorsque la méthode de chargement lourd m1 () se termine, le processus P1 se termine, ce qui doit libérer le tas sur cette machine virtuelle Java.

    Votre problème est complexe – je suggère une solution simple. D’après ce que j’ai lu, vous savez coder en C et en Java. Peut-être utiliser JNA (Java Native Access) ou JNI (Java Native Interface) peut résoudre le problème (coder la partie traitement lourd en C et l’appeler depuis Java). Une autre façon serait de faire un gros programme avec un autre petit programme en C – vous appeleriez ce petit programme à partir de votre code Java (de préférence dans son propre thread). Java Native Access Interface native Java