JVM TI: como criar um plug-in para uma máquina virtual



Deseja adicionar algum recurso útil à JVM? Teoricamente, todo desenvolvedor pode contribuir com o OpenJDK; no entanto, na prática, qualquer alteração não trivial no HotSpot não é muito bem-vinda e, mesmo com o atual ciclo de versão abreviado, pode levar anos até que os usuários do JDK vejam seu recurso.

No entanto, em alguns casos, é possível expandir a funcionalidade de uma máquina virtual sem sequer tocar em seu código. A Interface da ferramenta JVM, a API padrão para interagir com a JVM, ajuda.

No artigo, mostrarei com exemplos concretos o que pode ser feito com ele, contarei o que mudou no Java 9 e 11 e alertarei honestamente sobre as dificuldades (spoiler: preciso lidar com C ++).

Eu também falei sobre esse material no JPoint. Se você preferir o vídeo, poderá assistir ao relatório do vídeo .

Entrada


A rede social Odnoklassniki, onde trabalho como engenheiro líder, é quase inteiramente escrita em Java. Mas hoje vou falar sobre outra parte, que não é inteiramente em Java.

Como você sabe, o problema mais popular entre os desenvolvedores de Java é o NullPointerException. Uma vez, enquanto estava de serviço no portal, também me deparei com o NPE em produção. O erro foi acompanhado por algo como este rastreamento de pilha:



Obviamente, no rastreamento da pilha, é possível rastrear o local em que a exceção ocorreu até uma linha específica no código. Somente nesse caso, não me fez sentir melhor, porque aqui a NPE pode encontrar muitas coisas em que:



Seria ótimo se a JVM sugerisse exatamente onde esse erro ocorreu, por exemplo, assim:
java.lang.NullPointerException: Called 'getUsers()' method on null object

Mas, infelizmente, o NPE agora não contém nada desse tipo. Embora eles estejam solicitando isso há muito tempo, pelo menos com o Java 1.4: esse bug tem 16 anos. Periodicamente, mais e mais bugs eram abertos sobre esse tópico, mas eram invariavelmente fechados como "Won't Fix":



Isso não acontece em todos os lugares. Volker Simonis, da SAP, contou como eles implementaram esse recurso no SAP JVM por um longo tempo e o ajudaram mais de uma vez. Outro funcionário da SAP mais uma vez enviou um bug no OpenJDK e se ofereceu para implementar um mecanismo semelhante ao que está na SAP JVM. E eis que desta vez o bug não foi fechado - há uma chance de que esse recurso entre no JDK 14.

Mas quando o JDK 14 será lançado e quando mudaremos para ele? E se você quiser investigar o problema aqui e agora?

Obviamente, você pode manter seu fork do OpenJDK. O próprio recurso de relatório da NPE não é tão complicado que poderíamos muito bem ter implementado. Mas, ao mesmo tempo, haverá todos os problemas de apoiar sua própria assembléia. Seria ótimo implementar o recurso uma vez e simplesmente conectá-lo a qualquer versão da JVM como um plug-in. E isso é realmente possível! A JVM possui uma API especial (originalmente desenvolvida para todos os tipos de depuradores e criadores de perfil): JVM Tool Interface.

Mais importante ainda, essa API é padrão. Ele tem uma especificação estrita e, ao implementar um recurso de acordo com ele, você pode ter certeza de que ele funcionará em novas versões da JVM.

Para usar essa interface, você precisa escrever um programa pequeno (ou grande, dependendo de suas tarefas). Nativo: geralmente é escrito em C ou C ++. A jdk/include/jvmti.h JDK padrão possui um arquivo de cabeçalho jdk/include/jvmti.h que você deseja incluir.

O programa é compilado em uma biblioteca dinâmica e conectado pelo parâmetro -agentpath durante o início da JVM. É importante não confundi-lo com outro parâmetro semelhante: -javaagent . De fato, os agentes Java são um caso especial de agentes da JVM TI. Além disso, no texto sob a palavra "agente" entende-se precisamente o agente nativo.

Por onde começar


Vamos ver na prática como escrever o agente JVM TI mais simples, uma espécie de "olá mundo".

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

A primeira linha eu incluo o mesmo arquivo de cabeçalho. A seguir, vem a principal função que precisa ser implementada no agente: Agent_OnLoad() . A própria máquina virtual a chama quando o agente é inicializado, passando um ponteiro para o objeto JavaVM* .

Utilizando-o, você pode obter um ponteiro para o ambiente da JVM TI: jvmtiEnv* . E através dele, por sua vez, já chamamos as funções de TI da JVM. Por exemplo, usando GetSystemProperty, leia o valor de uma propriedade do sistema.

Se agora eu executar esse "olá mundo", passando o arquivo dll compilado para -agentpath , a linha impressa pelo nosso agente aparecerá no console antes do programa Java começar a ser executado:



Enriquecimento NPE


Como o hello world não é o exemplo mais interessante, voltemos às nossas exceções. O código completo do agente que complementa os relatórios NPE está no GitHub .

É assim que Agent_OnLoad() se quiser solicitar à máquina virtual que nos notifique sobre todas as exceções:

 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; } 

Primeiro, peço à TI da JVM o recurso correspondente (can_generate_exception_events). Falaremos sobre capacidade separadamente.

A próxima etapa é assinar os eventos de exceção. Sempre que a JVM lança exceções (não importa se são capturadas ou não), nossa função ExceptionCallback() será chamada.

A etapa final é chamar SetEventNotificationMode() para permitir a entrega de notificações.

No ExceptionCallback, a JVM passa tudo o que precisamos para lidar com exceções.
 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)); } 


Aqui há o objeto do encadeamento que lançou a exceção (encadeamento) e o local em que isso aconteceu (método, localização) e o objeto da exceção (exceção), e até o local no código que captura essa exceção (catch_method, catch_location).

O que é importante: nesse retorno de chamada, além do ponteiro para o ambiente da JVM TI, o ambiente da JNI (env) também é passado. Isso significa que podemos usar todas as funções JNI nele. Ou seja, JVM TI e JNI coexistem perfeitamente, complementando-se.

No meu agente eu uso os dois. Em particular, através da JNI, verifico se minha exceção é do tipo NullPointerException e substituo o campo detailMessage uma mensagem de erro.

Como a própria JVM nos passa o local - o índice de bytecode no qual a exceção ocorreu, basta colocar esse local aqui na mensagem:



O número 66 indica o índice no bytecode em que essa exceção ocorreu. Mas analisar o bytecode manualmente é sombrio: você precisa descompilar o arquivo de classe, procurar a 66ª instrução, tentar entender o que estava fazendo ... Seria ótimo se o próprio agente mostrasse algo mais legível por humanos.

No entanto, neste caso, a JVM TI tem tudo o que você precisa. É verdade que você precisa solicitar recursos adicionais da JVM TI: get bytecode e método de pool constante.

 jvmtiCapabilities capabilities = {0}; capabilities.can_generate_exception_events = 1; capabilities.can_get_bytecodes = 1; capabilities.can_get_constant_pool = 1; jvmti->AddCapabilities(&capabilities); 

Agora vou expandir o ExceptionCallback: através da função GetBytecodes() da JVM TI, vou obter o corpo do método para verificar o que está nele pelo índice de localização. Em seguida, vem uma grande chave de instruções do código de código: se este for um acesso à matriz, haverá uma mensagem de erro, se o acesso ao campo for outra mensagem, se a chamada do método for a terceira e assim por diante.

Código de exceção
 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); 


Resta apenas substituir o nome do campo ou método. Você pode obtê-lo no pool constante , que está disponível novamente graças à JVM TI.

 if (jvmti->GetConstantPool(holder, &cpool_count, &cpool_bytes, &cpool) != 0) { return strdup("<unknown>"); } 

A seguir vem um pouco de mágica, mas na realidade nada complicado, apenas de acordo com a especificação do formato do arquivo de classe, analisamos o pool constante e, a partir daí, isolamos a linha - o nome do método.

Análise de pool constante
 u1* ref = get_cpool_at(cpool, get_u2(bytecodes + 1)); // CONSTANT_Fieldref u1* name_and_type = get_cpool_at(cpool, get_u2(ref + 3)); // CONSTANT_NameAndType u1* name = get_cpool_at(cpool, get_u2(name_and_type + 1)); // CONSTANT_Utf8 size_t name_length = get_u2(name + 1); char* result = (char*) malloc(name_length + 1); memcpy(result, name + 3, name_length); result[name_length] = 0; 


Outro ponto importante: algumas funções da JVM TI, por exemplo, GetConstantPool() ou GetBytecodes() , alocam uma certa estrutura na memória nativa, que precisa ser liberada quando você terminar de trabalhar com ela.

 jvmti->Deallocate(cpool); 

Execute o programa de origem com nosso agente estendido e aqui está uma descrição completamente diferente da exceção: ele relata que chamamos o método longValue () no objeto nulo.



Outras aplicações


De um modo geral, os desenvolvedores geralmente desejam lidar com exceções à sua maneira. Por exemplo, reinicie automaticamente a JVM se StackOverflowError um StackOverflowError .

Esse desejo pode ser entendido, já que StackOverflowError é o mesmo erro fatal que OutOfMemoryError , após sua ocorrência, não é mais possível garantir a operação correta do programa. Ou, por exemplo, às vezes para analisar o problema, desejo receber um despejo de encadeamento ou dump de heap quando ocorrer uma exceção.



Para ser justo, o IBM JDK tem essa oportunidade pronta para uso. Mas agora já sabemos que, usando o agente da JVM TI, você pode implementar a mesma coisa no HotSpot. Basta assinar o retorno de chamada da exceção e analisar a exceção. Mas como remover despejo de encadeamento ou despejo de heap do nosso agente? A TI da JVM tem tudo o que você precisa para este caso:



Não é muito conveniente implementar todo o mecanismo de ignorar o heap e criar um dump. Mas vou compartilhar o segredo de como torná-lo mais fácil e rápido. É verdade que isso não está mais incluído na JVM TI padrão, mas é uma extensão privada do Hotspot.

Você precisa conectar o arquivo de cabeçalho jmm.h a partir das fontes HotSpot e chamar a função 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); } 

Ele retornará um ponteiro para a Interface de Gerenciamento do HotSpot, que em uma única chamada gerará um Heap Dump ou Thread Dump. O código completo para o exemplo pode ser encontrado na minha resposta ao Stack Overflow.

Naturalmente, você pode manipular não apenas exceções, mas também vários outros eventos relacionados à operação da JVM: iniciar / parar encadeamentos, carregar classes, coleta de lixo, métodos de compilação, métodos de entrada / saída e até mesmo acessar ou modificar campos específicos de objetos Java.

Eu tenho um exemplo de outro agente vmtrace que assina muitos eventos padrão da JVM TI e os registra. Se eu executar um programa simples com esse agente, obterá um log detalhado que, quando concluído, com registros de data e hora:



Como você pode ver, para simplesmente imprimir o hello world, centenas de classes são carregadas, dezenas e centenas de métodos são gerados e compilados. Fica claro por que o Java leva tanto tempo para ser executado. Tudo levou mais de duzentos milissegundos.

O que a JVM TI pode fazer


Além da manipulação de eventos, a JVM TI possui vários outros recursos. Eles podem ser divididos em dois grupos.

Uma é obrigatória, que qualquer JVM que suporte a TI da JVM deve implementar. Isso inclui as operações de análise de métodos, campos, fluxos, a capacidade de adicionar novas classes ao caminho de classe e assim por diante.

Existem recursos opcionais que requerem uma solicitação preliminar de recursos. A JVM não é necessária para oferecer suporte a todos eles, no entanto, o HotSpot implementa toda a especificação na íntegra. Os recursos opcionais são divididos em dois subgrupos: aqueles que podem ser conectados apenas no início da JVM (por exemplo, a capacidade de definir ponto de interrupção ou analisar variáveis ​​locais) e aqueles que podem ser conectados a qualquer momento (em particular, bytecode ou pool constante, que eu usado acima).



Você pode perceber que a lista de recursos é muito semelhante aos recursos do depurador. De fato, um depurador Java nada mais é do que um caso especial do agente da JVM TI, que tira proveito de todos esses recursos e solicita todos os recursos.

A separação de recursos entre os que podem ser ativados a qualquer momento e os que são apenas no momento da inicialização é feita de propósito. Nem todos os recursos são gratuitos, alguns carregam sobrecarga.

Se tudo estiver claro com as despesas gerais diretas que acompanham o uso do recurso, existem outras indiretas ainda menos óbvias que aparecem, mesmo que você não o utilize, mas, simplesmente, por meio dos recursos, você declara que será necessário em algum momento no futuro. Isso ocorre porque a máquina virtual pode compilar o código de maneira diferente ou adicionar verificações adicionais ao tempo de execução.

Por exemplo, o recurso já considerado para assinar exceções (can_generate_exception_events) leva ao fato de que todas as exceções lançadas ocorrerão lentamente. Em princípio, isso não é tão assustador, porque as exceções são raras em um bom programa Java.

A situação com variáveis ​​locais é um pouco pior. Para can_access_local_variables, que permite obter os valores das variáveis ​​locais a qualquer momento, é necessário desativar algumas otimizações importantes. Em particular, o Escape Analysis para de funcionar completamente, o que pode gerar uma sobrecarga perceptível: dependendo da aplicação, 5 a 10%.

Daí a conclusão: se você executar o Java com o agente de depuração ativado, mesmo sem usá-lo, os aplicativos serão executados mais lentamente. De qualquer forma, incluir um agente de depuração na produção não é uma boa ideia.

Vários recursos, por exemplo, definir um ponto de interrupção ou rastrear todas as entradas / saídas de um método, carregam uma sobrecarga muito mais séria. Em particular, alguns eventos da TI da JVM (FieldAccess, MethodEntry / Exit) funcionam apenas no intérprete.

Um agente é bom e dois é melhor


Você pode conectar vários agentes a um único processo, simplesmente especificando vários parâmetros -agentpath . Todos terão seu próprio ambiente de JVM TI. Isso significa que todos podem assinar seus recursos e interceptar seus eventos independentemente.

E se dois agentes se inscreveram no evento Breakpoint, e em um o breakpoint está definido em algum método, quando esse método é executado, o segundo agente receberá o evento?

Na realidade, essa situação não pode ocorrer (pelo menos no HotSpot JVM). Porque existem alguns recursos que apenas um dos agentes pode possuir a qualquer momento. Isso inclui breakpoint_events em particular. Portanto, se o segundo agente solicitar o mesmo recurso, ele receberá um erro de resposta.

Esta é uma conclusão importante: o agente sempre deve verificar o resultado da solicitação de recursos, mesmo se você estiver executando no HotSpot e saber que todos eles estão disponíveis. A especificação da JVM TI não diz nada sobre recursos exclusivos, mas o HotSpot possui esse recurso de implementação.

É verdade que o isolamento do agente nem sempre funciona perfeitamente. Durante o desenvolvimento do async-profiler, deparei-me com este problema: quando temos dois agentes e um solicita a geração de eventos de compilação de métodos, todos os agentes recebem esses eventos. Obviamente, eu arquivei um bug , mas você deve ter em mente que eventos que você não espera podem ocorrer no seu agente.

Uso em um programa regular


A JVM TI pode parecer algo muito específico para depuradores e criadores de perfil, mas também pode ser usada em um programa Java comum. Considere um exemplo.

O paradigma de programação reativa agora é difundido quando tudo é assíncrono, mas há um problema com esse paradigma.

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

Eu executo duas tarefas assíncronas que diferem apenas nos parâmetros. E se algo der errado, uma exceção é levantada:



No rastreamento da pilha, não está claro qual dessas tarefas causou o problema. Porque a exceção ocorre em um segmento completamente diferente, onde não temos contexto. Como entender em qual tarefa?

Como uma das soluções, você pode adicionar informações sobre onde a criamos ao construtor de nossa tarefa assíncrona:

 public AsyncTask(String arg) { this.arg = arg; this.location = getLocation(); } 

Ou seja, lembre-se da localização - um local específico no código, até a linha de onde o construtor foi chamado. E em caso de uma exceção para prometer:

 try { int n = Integer.parseInt(arg); } catch (Throwable e) { System.err.println("ParseTask failed at " + location); e.printStackTrace(); } 

Agora, quando ocorrer uma exceção, veremos que isso aconteceu na linha 14 no TaskRunner (onde a tarefa com o parâmetro BAD é criada):



Mas como obter o lugar no código de onde o construtor é chamado? Antes do Java 9, havia a única maneira legal de fazer isso: obter um rastreamento de pilha, pular alguns quadros irrelevantes e um pouco mais baixo na pilha será o local que nosso código chamou.

 String getLocation() { StackTraceElement caller = Thread.currentThread().getStackTrace()[3]; return caller.getFileName() + ':' + caller.getLineNumber(); } 

Mas há um problema. Obter o StackTrace completo é bem lento. Eu tenho um relatório inteiro dedicado a isso.

Isso não seria um problema tão grande se acontecesse raramente. Mas, por exemplo, temos um serviço da Web - um front-end que aceita solicitações HTTP. Esta é uma ótima aplicação, milhões de linhas de código. E para capturar erros de renderização, usamos um mecanismo semelhante: nos componentes para renderização, lembramos o local onde eles são criados. Temos milhões desses componentes, portanto, obter todos os rastreamentos da pilha leva um tempo tangível para iniciar o aplicativo, não apenas um minuto. Portanto, esse recurso foi desativado anteriormente na produção, embora para análise de problemas seja necessário na produção.

O Java 9 introduziu uma nova maneira de ignorar as pilhas de fluxo: StackWalker, que por meio da API de fluxo pode fazer tudo isso preguiçosamente, sob demanda. Ou seja, podemos pular o número certo de quadros e obter apenas um que nos interesse.

 String getLocation() { return StackWalker.getInstance().walk(s -> { StackWalker.StackFrame frame = s.skip(3).findFirst().get(); return frame.getFileName() + ':' + frame.getLineNumber(); }); } 

Funciona um pouco melhor do que obter o rastreamento completo da pilha, mas não por uma ordem de magnitude ou mesmo várias vezes. No nosso caso, acabou sendo uma vez e meia mais rápido:



Há um problema conhecido com a implementação abaixo do ideal do StackWalker e, provavelmente, ele será corrigido no JDK 13. Mas, novamente, o que devemos fazer agora no Java 8, onde o StackWalker nem é lento?

A TI da JVM vem em socorro novamente. Existe uma função GetStackTrace() que pode fazer tudo o que você precisa: obtenha um fragmento de um rastreamento de pilha de um determinado comprimento, iniciando no quadro especificado e não faça mais nada.

 GetStackTrace(jthread thread, jint start_depth, jint max_frame_count, jvmtiFrameInfo* frame_buffer, jint* count_ptr) 

Há apenas uma pergunta: como chamar a função JVM TI do nosso programa Java? Assim como qualquer outro método nativo: carregue a biblioteca nativa com System.loadLibrary() , onde será a implementação JNI do nosso método.

 public class StackFrame { public static native String getLocation(int depth); static { System.loadLibrary("stackframe"); } } 

Um ponteiro para o ambiente de TI da JVM pode ser obtido não apenas em Agent_OnLoad (), mas também enquanto o programa está sendo executado e para continuar a usá-lo a partir de métodos JNI nativos comuns:

 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 .

Imagine: durante a compilação do HotSpot, parte do código-fonte é gerado rapidamente através da transformação XSLT. Foi assim que o Enterprise retornou ao HotSpot.

Qual poderia ser a solução? Só não chame essas funções com muita frequência, tente armazenar em cache os resultados. Ou seja, se alguma informação jmethodID foi recebida, lembre-se localmente em seu agente. Aplicando esse armazenamento em cache no nível do agente, retornamos o desempenho ao nível anterior.

Conexão dinâmica


, 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] 

E para versões mais antigas do JDK, você pode usar o meu utilitário jattach . Por exemplo, o async-profiler pode conectar-se imediatamente a aplicativos em execução sem argumentos adicionais da JVM, em parte graças ao jattach.

Para usar a possibilidade de conexão dinâmica no seu agente da JVM TI, você precisa, além disso Agent_OnLoad(), implementar uma função semelhante Agent_OnAttach(). A única diferença: Agent_OnAttach()você não pode usar os recursos disponíveis apenas no momento da inicialização do agente.

É importante lembrar que você pode conectar dinamicamente a mesma biblioteca várias vezes, para que Agent_OnAttach()possa ser chamada repetidamente.

Vou demonstrar pelo exemplo. O IntelliJ IDEA estará no papel de produção: este também é um aplicativo Java, o que significa que também podemos conectar-se a ele em tempo real e fazer alguma coisa.

Encontraremos o ID do processo de nossa IDEA; em seguida, com o utilitário jattach, conectaremos a JVM da biblioteca TI patcher.dll a esse processo:
jattach 8648 load patcher.dll true

imediatamente, mudou a cor do menu para vermelho: o



que esse agente faz? Localiza todos os objetos Java da classe fornecida ( javax.swing.AbstractButton) e chama através do método JNI setBackground(). O código completo pode ser visto aqui .

O que há de novo no Java 9


A JVM TI existe há muito tempo e, apesar dos erros existentes, já existe uma API depurada bem estabelecida que não foi alterada por um longo tempo. As primeiras inovações significativas apareceram no Java 9.

Como você sabe, o Java 9 trouxe aos desenvolvedores a dor e o sofrimento associados aos módulos. Primeiro de tudo, tornou-se difícil usar os "segredos" do JDK, sem os quais, às vezes, em princípio, não é possível.

Por exemplo, no JDK não há maneira legal de limpar o Direct ByteBuffer. Somente por meio de uma API privada:



digamos, no Cassandra, não há nenhum lugar sem esse recurso, porque todo o trabalho do DBMS é baseado no trabalho com o MappedByteBuffer e, se você não os limpar manualmente, a JVM travará rapidamente.

E se você tentar executar o mesmo código no JDK 9, obterá o IllegalAccessError:



A situação com o Reflection é aproximadamente a mesma: tornou-se difícil chegar a campos particulares.

Por exemplo, nem todas as operações de arquivo do Linux estão disponíveis em Java. Portanto, para recursos específicos do Linux, os programadores recuperaram o java.io.FileDescriptordescritor de arquivo do sistema do objeto através de reflexão e, usando JNI, denominaram algumas funções do sistema. E agora, se você executar isso no JDK 9, verá maldições nos logs:



Obviamente, existem sinalizadores da JVM que abrem os módulos privados necessários e permitem o uso de classes e reflexões privadas. Mas você precisa registrar manualmente todos os pacotes que você pretende usar. Por exemplo, para executar apenas o Cassandra no Java 11, é necessário registrar um banner:

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

No entanto, juntamente com os módulos, as funções da JVM TI para trabalhar com eles apareceram:

  • GetAllModules
  • AddModuleExports
  • AddModuleOpens
  • etc.

Observando esta lista, a solução se sugere: você pode esperar o carregamento da JVM, obter uma lista de todos os módulos, revisar todos os pacotes, abrir tudo para todos e aproveitar.

Aqui está o mesmo exemplo com o 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"); } 

Se o executarmos sem agentes, esperamos obter um IllegalAccessError. E se você adicionar um agente antimódulo escrito por mim ao caminho do agente , o exemplo funcionará sem erros. A mesma coisa com a reflexão.

O que há de novo no Java 11


Outra inovação apareceu no Java 11. É apenas uma, mas que a! Apareceu a possibilidade de criação de perfil leve de alocações: foi adicionado um novo evento SampledObjectAlloc, no qual você pode se inscrever, para que recebam notificações seletivas sobre alocações.

Tudo o que é necessário para uma análise mais aprofundada será transferido para o retorno de chamada: o thread que aloca, o próprio objeto selecionado, sua classe, tamanho. Outro método SetHeapSampingIntervalé alterar a frequência da frequência dessas notificações.



Por que isso é necessário? A criação de perfil de alocação foi anterior em todos os criadores de perfil populares, mas trabalhou com a instrumentação, que é repleta de sobrecarga. A única ferramenta de perfil com baixo custo operacional foi o Java Flight Recorder.

A idéia do novo método é instrumentar não todas as alocações, mas apenas algumas delas, em outras palavras, para amostrar.

No caso mais rápido e frequente, a alocação ocorre no Buffer de Alocação Local do Encadeamento, simplesmente aumentando o ponteiro. E com a inclusão da amostragem no TLAB, é adicionada uma borda virtual correspondente à frequência de amostragem. Assim que a próxima alocação exceder esse limite, um evento será enviado sobre a alocação do objeto.



Em alguns casos, objetos grandes que não se encaixam no TLAB são alocados diretamente no heap. Esses objetos também seguem o caminho de alocação lento pelo tempo de execução da JVM e também são amostrados.

Devido ao fato de que agora a amostragem é realizada apenas para alguns objetos, a sobrecarga já é aceitável para produção - na maioria dos casos, inferior a 5%.

Curiosamente, esse recurso existe há muito tempo, desde a época do JDK 7, criado especificamente para o Flight Recorder. Porém, através da API privada do Hotspot, o async-profiler também usou isso. E agora, começando com o JDK 11, essa API tornou-se pública, entrou na TI da JVM e outros criadores de perfil podem usá-la. Em particular, o YourKit já sabe como. E como usar essa API, você pode ver no exemplo publicado em nosso repositório.

Usando esse criador de perfil, você pode criar belos diagramas de alocação. Observe quais objetos se destacam, quantos deles se destacam e, mais importante, onde.



Conclusão


A JVM TI é uma ótima maneira de interagir com uma máquina virtual.

Os plug-ins gravados em C ou C ++ podem ser iniciados no início da JVM ou podem ser conectados dinamicamente diretamente enquanto o aplicativo está em execução. Além disso, o próprio aplicativo pode usar as funções da JVM TI através de métodos nativos.

Todos os exemplos demonstrados são publicados em nosso repositório no GitHub . Use, estude e faça perguntas.

Source: https://habr.com/ru/post/pt458812/


All Articles