
Souhaitez-vous ajouter des fonctionnalités utiles à la JVM? Théoriquement, chaque développeur peut contribuer à OpenJDK, cependant, dans la pratique, toute modification non triviale de HotSpot n'est pas la bienvenue du côté, et même avec le cycle de publication raccourci actuel, il peut s'écouler des années avant que les utilisateurs de JDK voient votre fonctionnalité.
Néanmoins, dans certains cas, il est possible d'étendre les fonctionnalités d'une machine virtuelle sans même toucher à son code. L'interface de l'outil JVM, l'API standard pour interagir avec la JVM, est utile.
Dans l'article, je vais montrer avec des exemples concrets ce qui peut être fait avec, dire ce qui a changé en Java 9 et 11 et honnêtement mettre en garde contre les difficultés (spoiler: je dois faire face à C ++).
J'ai également parlé de ce matériel sur JPoint. Si vous préférez la vidéo, vous pouvez regarder le rapport
vidéo .
Entrée
Le réseau social Odnoklassniki, où je travaille en tant qu'ingénieur principal, est presque entièrement écrit en Java. Mais aujourd'hui, je vais vous parler d'une autre partie, qui n'est pas entièrement en Java.
Comme vous le savez, le problème le plus courant chez les développeurs Java est NullPointerException. Une fois, en service sur le portail, j'ai également rencontré NPE en production. L'erreur était accompagnée de quelque chose comme cette trace de pile:

Bien sûr, sur la trace de la pile, vous pouvez tracer l'endroit où l'exception s'est produite jusqu'à une ligne spécifique dans le code. Seulement dans ce cas, cela ne m'a pas fait me sentir mieux, car ici les NPE peuvent se rencontrer beaucoup où:

Ce serait formidable si la JVM suggérait exactement où était cette erreur, par exemple, comme ceci:
java.lang.NullPointerException: Called 'getUsers()' method on null object
Mais, malheureusement, NPE ne contient plus rien de tel. Bien qu'ils le demandent depuis longtemps, au moins avec Java 1.4:
ce bug a 16 ans. Périodiquement, de plus en plus de bugs ont été ouverts sur ce sujet, mais ils ont toujours été fermés en tant que "Won't Fix":

Cela ne se produit pas partout. Volker Simonis de SAP a
expliqué comment il avait implémenté cette fonctionnalité dans SAP JVM pendant longtemps et l'avait aidée plusieurs fois. Un autre employé de SAP a de nouveau
soumis un bogue dans OpenJDK et s'est porté volontaire pour implémenter un mécanisme similaire à celui de SAP JVM. Et, voilà, cette fois, le bogue n'a pas été fermé - il y a une chance que cette fonctionnalité entre dans JDK 14.
Mais quand le JDK 14 sortira-t-il et quand y passerons-nous? Que faire si vous souhaitez étudier le problème ici et maintenant?
Vous pouvez, bien sûr, maintenir votre fork d'OpenJDK. La fonctionnalité de rapport NPE elle-même n'est pas si compliquée, nous aurions très bien pu l'implémenter. Mais en même temps, il y aura tous les problèmes de support de votre propre assemblage. Ce serait formidable d'implémenter la fonctionnalité une fois, puis de la connecter simplement à n'importe quelle version de la JVM en tant que plugin. Et c'est vraiment possible! La JVM possède une API spéciale (développée à l'origine pour toutes sortes de débogueurs et de profileurs): JVM Tool Interface.
Plus important encore, cette API est standard. Il a une
spécification stricte et lorsque vous implémentez une fonctionnalité conformément à celle-ci, vous pouvez être sûr qu'elle fonctionnera dans les nouvelles versions de la JVM.
Pour utiliser cette interface, vous devez écrire un petit (ou grand, selon vos tâches) programme. Natif: il est généralement écrit en C ou C ++. La
jdk/include/jvmti.h
JDK standard possède un fichier d'en-tête
jdk/include/jvmti.h
que vous souhaitez inclure.
Le programme est compilé dans une bibliothèque dynamique et connecté par le paramètre
-agentpath
lors du démarrage de la JVM. Il est important de ne pas le confondre avec un autre paramètre similaire:
-javaagent
. En fait, les agents Java sont un cas particulier des agents JVM TI. Plus loin dans le texte sous le mot "agent", on entend précisément l'agent natif.
Par où commencer
Voyons en pratique comment écrire l'agent JVM TI le plus simple, une sorte de "bonjour le monde".
#include <jvmti.h> #include <stdio.h> JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { jvmtiEnv* jvmti; vm->GetEnv((void**) &jvmti, JVMTI_VERSION_1_0); char* vm_name = NULL; jvmti->GetSystemProperty("java.vm.name", &vm_name); printf("Agent loaded. JVM name = %s\n", vm_name); fflush(stdout); return 0; }
La première ligne, j'inclus le même fichier d'en-tête. Vient ensuite la fonction principale qui doit être implémentée dans l'agent:
Agent_OnLoad()
. La machine virtuelle elle-même l'appelle au démarrage de l'agent, en passant un pointeur sur l'objet
JavaVM*
.
En l'utilisant, vous pouvez obtenir un pointeur vers l'environnement JVM TI:
jvmtiEnv*
. Et à travers elle, à son tour, déjà appeler JVM TI-fonctions. Par exemple, à l'aide de
GetSystemProperty, lisez la valeur d'une propriété système.
Si maintenant
-agentpath
ce "bonjour le monde" en passant le fichier dll compilé à
-agentpath
, la ligne imprimée par notre agent apparaîtra dans la console avant que le programme Java ne démarre:

Enrichissement NPE
Puisque Hello World n'est pas l'exemple le plus intéressant, revenons à nos exceptions. Le code d'agent complet qui complète les rapports NPE se trouve
sur GitHub .
Voici à quoi
Agent_OnLoad()
si je veux demander à la machine virtuelle de nous informer de toutes les exceptions:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { jvmtiEnv* jvmti; vm->GetEnv((void**) &jvmti, JVMTI_VERSION_1_0); jvmtiCapabilities capabilities = {0}; capabilities.can_generate_exception_events = 1; jvmti->AddCapabilities(&capabilities); jvmtiEventCallbacks callbacks = {0}; callbacks.Exception = ExceptionCallback; jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL); return 0; }
Je demande d'abord à la JVM TI la capacité correspondante (can_generate_exception_events). Nous parlerons de la capacité séparément.
L'étape suivante consiste à s'abonner aux événements d'exception. Chaque fois que la JVM lève des exceptions (qu'elles soient interceptées ou non), notre fonction
ExceptionCallback()
sera appelée.
La dernière étape consiste à appeler
SetEventNotificationMode()
pour activer la remise des notifications.
Dans ExceptionCallback, la JVM transmet tout ce dont nous avons besoin pour gérer les exceptions. void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, jthread thread, jmethodID method, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location) { jclass NullPointerException = env->FindClass("java/lang/NullPointerException"); if (!env->IsInstanceOf(exception, NullPointerException)) { return; } jclass Throwable = env->FindClass("java/lang/Throwable"); jfieldID detailMessage = env->GetFieldID(Throwable, "detailMessage", "Ljava/lang/String;"); if (env->GetObjectField(exception, detailMessage) != NULL) { return; } char buf[32]; sprintf(buf, "at location %id", (int) location); env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf)); }
Ici, il y a à la fois l'objet du thread qui a levé l'exception (thread), et l'endroit où cela s'est produit (méthode, emplacement), et l'objet de l'exception (exception), et même l'endroit dans le code qui capture cette exception (catch_method, catch_location).
Ce qui est important: dans ce rappel, en plus du pointeur vers l'environnement JVM TI, l'environnement JNI (env) est également passé. Cela signifie que nous pouvons y utiliser toutes les fonctions JNI. Autrement dit, JVM TI et JNI coexistent parfaitement, se complétant mutuellement.
Dans mon agent, j'utilise les deux. En particulier, via JNI, je vérifie que mon exception est de type
NullPointerException
, puis je remplace le champ
detailMessage
un message d'erreur.
Puisque la JVM elle-même nous transmet l'emplacement - l'index de bytecode sur lequel l'exception s'est produite, alors je viens de mettre cet emplacement ici dans le message:

Le nombre 66 indique l'index en bytecode où cette exception s'est produite. Mais l'analyse manuelle du bytecode est morne: vous devez décompiler le fichier de classe, rechercher la 66e instruction, essayer de comprendre ce qu'elle faisait ... Ce serait formidable si notre agent lui-même pouvait montrer quelque chose de plus lisible par l'homme.
Cependant, dans ce cas, la JVM TI a tout ce dont vous avez besoin. Certes, vous devez demander des fonctionnalités supplémentaires de la JVM TI: obtenir le bytecode et la méthode du pool constant.
jvmtiCapabilities capabilities = {0}; capabilities.can_generate_exception_events = 1; capabilities.can_get_bytecodes = 1; capabilities.can_get_constant_pool = 1; jvmti->AddCapabilities(&capabilities);
Maintenant, je vais développer ExceptionCallback: grâce à la fonction JVM TI
GetBytecodes()
je vais obtenir le corps de la méthode pour vérifier ce qu'il
GetBytecodes()
par index de localisation. Vient ensuite une grande instruction de bytecode de commutateur: s'il s'agit d'un accès au tableau, il y aura un message d'erreur, si l'accès au champ est un autre message, si l'appel de méthode est le troisième, et ainsi de suite.
Code de rappel d'exception jint bytecode_count; u1* bytecodes; if (jvmti->GetBytecodes(method, &bytecode_count, &bytecodes) != 0) { return; } if (location >= 0 && location < bytecode_count) { const char* message = get_exception_message(bytecodes[location]); if (message != NULL) { ... env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf)); } } jvmti->Deallocate(bytecodes);
Il ne reste plus qu'à substituer le nom du champ ou de la méthode. Vous pouvez l'obtenir à partir du
pool constant , qui est à nouveau disponible grâce à la JVM TI.
if (jvmti->GetConstantPool(holder, &cpool_count, &cpool_bytes, &cpool) != 0) { return strdup("<unknown>"); }
Vient ensuite un peu de magie, mais en réalité rien de délicat, juste conformément à
la spécification du format de fichier de classe, nous analysons le pool constant et à partir de là, nous isolons la ligne - le nom de la méthode.
Analyse du pool constant u1* ref = get_cpool_at(cpool, get_u2(bytecodes + 1));
Autre point important: certaines fonctions JVM TI, par exemple
GetConstantPool()
ou
GetBytecodes()
, allouent une certaine structure en mémoire native, qui doit être libérée lorsque vous avez fini de travailler avec.
jvmti->Deallocate(cpool);
Exécutez le programme source avec notre agent étendu, et voici une description complètement différente de l'exception: il signale que nous avons appelé la méthode longValue () sur l'objet null.

Autres applications
De manière générale, les développeurs souhaitent souvent gérer les exceptions à leur manière. Par exemple, redémarrez automatiquement la JVM si une
StackOverflowError
.
Ce désir peut être compris, car
StackOverflowError
est la même erreur fatale que
OutOfMemoryError
, après son occurrence, il n'est plus possible de garantir le bon fonctionnement du programme. Ou, par exemple, parfois pour analyser le problème, je souhaite recevoir un vidage de thread ou un vidage de tas lorsqu'une exception se produit.

En toute équité, l'IBM JDK a une telle opportunité hors de la boîte. Mais maintenant, nous savons déjà qu'en utilisant l'agent JVM TI, vous pouvez implémenter la même chose dans HotSpot. Il suffit de s'abonner au rappel d'exception et d'analyser l'exception. Mais comment supprimer le vidage de thread ou le vidage de tas de notre agent? La JVM TI a tout ce dont vous avez besoin pour ce cas:

Il n'est pas très pratique d'implémenter l'ensemble du mécanisme de contournement du tas et de création d'un vidage. Mais je vais partager le secret de la façon de le rendre plus facile et plus rapide. Certes, ce n'est plus inclus dans la JVM TI standard, mais c'est une extension privée de Hotspot.
Vous devez connecter le fichier d'en-tête
jmm.h à partir des sources HotSpot et appeler la fonction
JVM_GetManagement()
:
#include "jmm.h" JNIEXPORT void* JNICALL JVM_GetManagement(jint version); void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, ...) { JmmInterface* jmm = (JmmInterface*) JVM_GetManagement(JMM_VERSION_1_0); jmm->DumpHeap0(env, env->NewStringUTF("dump.hprof"), JNI_FALSE); }
Il renverra un pointeur vers l'interface de gestion HotSpot, qui en un seul appel générera un vidage de tas ou un vidage de thread. Le code complet de l'exemple se trouve dans
ma réponse à Stack Overflow.
Naturellement, vous pouvez gérer non seulement des exceptions, mais également un tas d'autres événements liés à l'opération JVM: démarrage / arrêt de threads, chargement de classes, garbage collection, méthodes de compilation, entrée / sortie de méthodes, même accès ou modification de champs spécifiques d'objets Java.
J'ai un exemple d'un autre agent
vmtrace qui souscrit à de nombreux événements JVM TI standard et les enregistre. Si je lance un programme simple avec cet agent, j'obtiendrai un journal détaillé, qui, une fois terminé, avec des horodatages:

Comme vous pouvez le voir, pour simplement imprimer Hello World, des centaines de classes sont chargées, des dizaines et des centaines de méthodes sont générées et compilées. Il devient clair pourquoi Java prend autant de temps à fonctionner. Tout sur tout a pris plus de deux cents millisecondes.
Ce que JVM TI peut faire
En plus de la gestion des événements, la JVM TI possède un tas d'autres fonctionnalités. Ils peuvent être divisés en deux groupes.
L'une est obligatoire, et toute JVM qui prend en charge la JVM TI doit l'implémenter. Il s'agit notamment des opérations d'analyse des méthodes, des champs, des flux, de la possibilité d'ajouter de nouvelles classes au chemin de classe, etc.
Il existe des fonctionnalités optionnelles qui nécessitent une demande de capacités préliminaire. JVM n'est pas tenu de les prendre en charge tous, cependant HotSpot implémente l'intégralité de la spécification. Les fonctionnalités optionnelles sont divisées en deux sous-groupes: ceux qui ne peuvent être connectés qu'au début de la machine virtuelle Java (par exemple, la possibilité de définir un point d'arrêt ou d'analyser des variables locales), et ceux qui peuvent être connectés à tout moment (en particulier, bytecode ou pool constant, que je utilisé ci-dessus).

Vous pouvez remarquer que la liste des fonctionnalités est très similaire aux capacités du débogueur. En fait, un débogueur Java n'est rien de plus qu'un cas particulier de l'agent JVM TI, qui tire parti de toutes ces capacités et demande toutes les capacités.
La séparation des capacités entre celles qui peuvent être activées à tout moment et celles qui ne le sont qu'au démarrage est effectuée exprès. Toutes les fonctionnalités ne sont pas gratuites, certaines comportent des frais généraux.
Si tout est clair avec les frais généraux directs qui accompagnent l'utilisation de la fonctionnalité, alors il y a encore moins indirects qui apparaissent même si vous n'utilisez pas la fonctionnalité, mais simplement grâce à des capacités, vous déclarez que cela sera nécessaire dans le futur. Cela est dû au fait que la machine virtuelle peut compiler le code différemment ou ajouter des vérifications supplémentaires à l'exécution.
Par exemple, la capacité déjà envisagée de s'abonner à des exceptions (can_generate_exception_events) conduit au fait que toutes les exceptions de lancement se feront lentement. En principe, ce n'est pas si effrayant, car les exceptions sont une chose rare dans un bon programme Java.
La situation avec les variables locales est légèrement pire. Pour can_access_local_variables, qui vous permet d'obtenir les valeurs des variables locales à tout moment, vous devez désactiver certaines optimisations importantes. En particulier, Escape Analysis cesse complètement de fonctionner, ce qui peut entraîner une surcharge notable: selon l'application, 5 à 10%.
D'où la conclusion: si vous exécutez Java avec l'agent de débogage activé, sans même l'utiliser, les applications s'exécuteront plus lentement. Quoi qu'il en soit, inclure un agent de débogage dans la production n'est pas une bonne idée.
Un certain nombre de fonctionnalités, par exemple, la définition d'un point d'arrêt ou le traçage de toutes les entrées / sorties d'une méthode, entraînent une surcharge beaucoup plus importante. En particulier, certains événements JVM TI (FieldAccess, MethodEntry / Exit) ne fonctionnent que dans l'interpréteur.
Un agent est bon et deux, c'est mieux
Vous pouvez connecter plusieurs agents à un seul processus en spécifiant simplement plusieurs paramètres
-agentpath
. Chacun aura son propre environnement JVM TI. Cela signifie que chacun peut souscrire à ses capacités et intercepter ses événements de manière indépendante.
Et si deux agents se sont abonnés à l'événement Breakpoint, et dans l'un, le point d'arrêt est défini dans une méthode, alors lorsque cette méthode est exécutée, le deuxième agent recevra-t-il l'événement?
En réalité, une telle situation ne peut pas se produire (au moins dans HotSpot JVM). Parce qu'il existe certaines capacités que seul un des agents peut posséder à un moment donné. Il s'agit notamment de breakpoint_events. Par conséquent, si le deuxième agent demande la même capacité, il recevra une erreur en réponse.
Il s'agit d'une conclusion importante: l'agent doit toujours vérifier le résultat de la demande de capacités, même si vous exécutez sur HotSpot et sachez que tous sont disponibles. La spécification JVM TI ne dit rien sur les capacités exclusives, mais HotSpot a une telle fonctionnalité de mise en œuvre.
Certes, l'isolement des agents ne fonctionne pas toujours parfaitement. Pendant le développement de
async-profiler, je suis tombé sur ce problème: lorsque nous avons deux agents et que l'un demande la génération d'événements de compilation de méthodes, tous les agents reçoivent ces événements. Bien sûr, j'ai déposé un
bogue , mais vous devez garder à l'esprit que des événements auxquels vous ne vous attendez pas peuvent se produire dans votre agent.
Utilisation dans un programme régulier
JVM TI peut sembler être une chose très spécifique pour les débogueurs et les profileurs, mais il peut également être utilisé dans un programme Java standard. Prenons un exemple.
Le paradigme de programmation réactive est maintenant répandu lorsque tout est asynchrone, mais il y a un problème avec ce paradigme.
public class TaskRunner { private static void good() { CompletableFuture.runAsync(new AsyncTask(GOOD)); } private static void bad() { CompletableFuture.runAsync(new AsyncTask(BAD)); } public static void main(String[] args) throws Exception { good(); bad(); Thread.sleep(200); } }
J'exécute deux tâches asynchrones qui ne diffèrent que par les paramètres. Et si quelque chose se passe mal, une exception est levée:

À partir de la trace de la pile, il est complètement difficile de déterminer laquelle de ces tâches a provoqué le problème. Parce que l'exception se produit dans un thread complètement différent, où nous n'avons pas de contexte. Comment comprendre dans quelle tâche?
Comme l'une des solutions, vous pouvez ajouter des informations sur l'endroit où nous l'avons créé au constructeur de notre tâche asynchrone:
public AsyncTask(String arg) { this.arg = arg; this.location = getLocation(); }
Autrement dit, rappelez-vous l'emplacement - un endroit spécifique dans le code, jusqu'à la ligne d'où le constructeur a été appelé. Et en cas d'exception à le mettre en gage:
try { int n = Integer.parseInt(arg); } catch (Throwable e) { System.err.println("ParseTask failed at " + location); e.printStackTrace(); }
Maintenant, lorsqu'une exception se produit, nous verrons que cela s'est produit sur la ligne 14 dans le TaskRunner (où la tâche avec le paramètre BAD est créée):

Mais comment obtenir la place dans le code d'où le constructeur est appelé? Avant Java 9, il y avait le seul moyen légal de le faire: obtenir une trace de pile, ignorer quelques images non pertinentes, et un peu plus bas sur la pile sera l'endroit que notre code appelait.
String getLocation() { StackTraceElement caller = Thread.currentThread().getStackTrace()[3]; return caller.getFileName() + ':' + caller.getLineNumber(); }
Mais il y a un problème. Obtenir le StackTrace complet est assez lent. J'ai tout un
rapport consacré à cela.
Ce ne serait pas un si gros problème si cela arrivait rarement. Mais, par exemple, nous avons un service Web - un frontend qui accepte les requêtes HTTP. Ceci est une excellente application, des millions de lignes de code. Et pour détecter les erreurs de rendu, nous utilisons un mécanisme similaire: dans les composants de rendu, nous nous souvenons de l'endroit où ils sont créés. Nous avons des millions de ces composants, donc obtenir toutes les traces de pile prend un temps tangible pour démarrer l'application, pas seulement une minute. Par conséquent, cette fonctionnalité était auparavant désactivée en production, bien que pour l'analyse des problèmes, elle soit nécessaire en production.
Java 9 a introduit une nouvelle façon de contourner les piles de flux: StackWalker, qui via l'API Stream peut faire tout cela paresseusement, à la demande. Autrement dit, nous pouvons sauter le bon nombre d'images et n'en obtenir qu'une qui nous intéresse.
String getLocation() { return StackWalker.getInstance().walk(s -> { StackWalker.StackFrame frame = s.skip(3).findFirst().get(); return frame.getFileName() + ':' + frame.getLineNumber(); }); }
Cela fonctionne un peu mieux que d'obtenir la trace complète de la pile, mais pas par ordre de grandeur ou même plusieurs fois. Dans notre cas, cela s'est avéré environ une fois et demie plus rapide:

Il y a un
problème connu avec l'implémentation sous-optimale de StackWalker, et très probablement il sera même corrigé dans JDK 13. Mais encore une fois, que devons-nous faire maintenant en Java 8, où StackWalker n'est même pas lent?
La JVM TI revient à la rescousse. Il existe une fonction
GetStackTrace()
qui peut faire tout ce dont vous avez besoin: obtenir un fragment d'une trace de pile d'une longueur donnée, à partir du cadre spécifié, et ne rien faire de plus.
GetStackTrace(jthread thread, jint start_depth, jint max_frame_count, jvmtiFrameInfo* frame_buffer, jint* count_ptr)
Il ne reste qu'une question: comment appeler la fonction JVM TI depuis notre programme Java? Comme toute autre méthode native: chargez la bibliothèque native avec
System.loadLibrary()
, où sera l'implémentation JNI de notre méthode.
public class StackFrame { public static native String getLocation(int depth); static { System.loadLibrary("stackframe"); } }
Un pointeur vers l'environnement JVM TI
peut être obtenu non seulement à partir d'Agent_OnLoad (), mais également pendant l'exécution du programme, puis à l'utiliser à partir de méthodes JNI natives ordinaires:
JNIEXPORT jstring JNICALL Java_StackFrame_getLocation(JNIEnv* env, jclass unused, jint depth) { jvmtiFrameInfo frame; jint count; jvmti->GetStackTrace(NULL, depth, 1, &frame, &count);
:

, JDK : - . -. , , , JDK. JDK 8u112, JVM TI-, (GetMethodName, GetMethodDeclaringClass ), .
, , : JVM TI- , , -. , C++,
jvmtiEnter.xsl .
: HotSpot XSLT-. HotSpot.
? , . , - jmethodID , . , .
, JVM TI Java- ,
System.loadLibrary
.
, , JVM TI-
-agentpath
JVM.
: (dynamic attach).
? , - , , JVM TI- .
JDK 9, jcmd:
jcmd <pid> JVMTI.agent_load /path/to/agent.so [arguments]
JDK
jattach . ,
async-profiler , - JVM-, jattach.
JVM TI- , ,
Agent_OnLoad()
,
Agent_OnAttach()
. :
Agent_OnAttach()
capabilities, .
, ,
Agent_OnAttach()
.
. IntelliJ IDEA: Java-, , - .
process ID IDEA, jattach JVM TI- patcher.dll:
jattach 8648 load patcher.dll true
:

? Java- (
javax.swing.AbstractButton
) JNI
setBackground()
.
.
Java 9
JVM TI , , , API, . Java 9.
, Java 9 , . , «» JDK, .
, JDK Direct ByteBuffer. API:

, Cassandra , MappedByteBuffer, , JVM .
JDK 9, IllegalAccessError:

Reflection: .
, Java Linux. -
java.io.FileDescriptor
JNI - . , JDK 9, :

, JVM, . , . , Cassandra Java 11,
:
--add-exports java.base/jdk.internal.misc=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED --add-exports java.sql/java.sql=ALL-UNNAMED --add-opens java.base/java.lang.module=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.math=ALL-UNNAMED --add-opens java.base/jdk.internal.module=ALL-UNNAMED --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
JVM TI :
- GetAllModules
- AddModuleExports
- AddModuleOpens
- . .
, : JVM, , , .
Direct ByteBuffer:
public static void main(String[] args) { ByteBuffer buf = ByteBuffer.allocateDirect(1024); ((sun.nio.ch.DirectBuffer) buf).cleaner().clean(); System.out.println("Buffer cleaned"); }
, IllegalAccessError. agentpath
antimodule , . .
Java 11
Java 11. , ! :
SampledObjectAlloc
, , .
callback , : , , , , .
SetHeapSampingInterval
, .

? , , . Java Flight Recorder.
, , , , .
Thread Local Allocation Buffer . TLAB , . , .

, TLAB, . JVM runtime .
, , — 5%.
, , JDK 7, Flight Recorder. API async-profiler. , JDK 11, API , JVM TI, . , YourKit . API,
, .
. , , , , .

Conclusion
JVM TI — .
, ++, JVM . , JVM TI .
GitHub . , .