Curiosidades, Java, JVM

Weblogic e Java Classloading: parte 1

Neste post descrevo um pouco sobre o modelo de Classloading Java. Na sequência pretendo escrever sobre Classloading no contexto do Oracle Weblogic Application Server. A fonte de referência usada neste post é a excelente documentação oficial do Weblogic [1].

O mecanismo de Classloading é uma “peça” importantíssima dentro da Máquina Virtual Java (JVM). É o mecanismo responsável por encontrar e carregar uma Classe Java na memória da JVM em tempo de execução. Quando uma aplicação referencia uma Classe Java em tempo de execução, é o Classloader (CL) quem localiza e carrega sua definição na Memória Heap. Nesse processo de carga, caso a classe não seja localizada no Classpath ocorre a famosa ClassNotfoundException (dentre outras exceptions associadas à este tipo de problema).

Classloader hierárquico

Por definição a JVM trabalha com uma hierarquia de Classloader (Java Classloader Hierarchy) semelhante ao relacionamento hierárquico entre classes Java (Superclasses e Subclasses). Na raiz dessa hierarquia está o bootstrap classloader – carregado pela JVM e composto por classes internas distribuídas pelas bibliotecas da JDK (classes pertencentes ao pacote java.*) – geralmente localizadas em <JAVA_HOME>/jre/lib/rt.jar.

No segundo nível do CL está o extensions classloader. Neste CL são carregadas as classes localizadas em <JAVA_HOME>/jre/lib/ext. Utilizar este diretório é uma forma de estender o CL padrão do Java. Entretanto as bibliotecas localizadas nesse diretório devem ser autocontidas, ou seja, não podem depender de classes/bibliotecas externas.

Estendendo o “extension classloader” está o system classloader, responsável por carregar classes contidas no classpath utilizado para carregar a JVM. Seguindo a hierarquiva vem o “application classloader“. A partir desse nível são definidos os classloaders específicos da aplicação (por exemplo o classloader carregado pelo Weblogic).

NOTA: No contexto do Weblogic, o termo utilizado pela Oracle para referenciar o classloader utilizado pelo Applciation Server é “system classloader”. Nesse mesmo contxto o temo “application classloader” refere-se às bibliotecas e aplicações Java Enterprise Edition (JavaEE).

A imagem abaixo [2] mostra a hierarquia padrão do modelo de classloader do Java.

clhierarchy

Carregamento (loading) de classes

De acordo com o artigo “Demystifying class loading problems, Part 1: An introduction to class loading and debugging tools” [2] publicado no site IBM developerWorks, o processo de carregamento de uma Classe Java pode ser separado em três momentos distintos: loading, linking e initializing.

A imagem abaixo [2] mostra o processo de class loading de uma Classe Java.

cl phases

Primeiro (loading phase) o arquivo (.class) binário da classe é localizado (dentro da lista de classes/bibliotecas informada no classpath usado pela JVM) e carregado no Bytecode. O processo de loading define, internamente na JVM, uma estrutura básica para que a classe seja carregada em memória.

No segundo momento o processo de linking realiza três passos adicionais:

  • Bytecode verification: neste passo o classloader se certifica de que o bytecode da classe está ok.
  • Class preparation: neste passo a estrutura necessária para acomodar a definição da classe (métodos, campos, interfaces implementadas, etc) é preparada.
  • Resolving: neste passo o classloader carrega todas as dependências referenciadas pela classe em questão. Pode-se entender como a montagem do grafo de dependências referenciadas por: hierarquia de classes pais (superclasses), interfaces, campos, assinatura de métodos, etc.

No terceiro e último momento (initializing) o conteúdo estático (static fields, static blocks, etc) da classe é inicializado.

Após esse processo a Classe é considerada como carregada e pode ser utilizada pela aplicação.

Modelo de Delegação (delegation model)

Outro aspecto importante relacionado ao Classloader é o modelo utilizado para carregamento de uma classe. Antes de falar sobre o modelo de delegação em si vale a pena comentar um pouco sobre o “disparo” de carregamento de uma Classe Java. Existe basicamente duas formas de iniciar (disparar/delegar) o carregamento de uma classe: carregamento explícito e carregamento implícito.

Na primeira forma a classe é carregada explicitamente usando as seguintes chamadas no código Java:

 cl.loadClass("com.mycompany.MyClass");  //cl é uma instância de java.lang.ClassLoader
 Class.forName("com.mycompany.MyClass"); //o classloader pais neste caso é o mesmo usado para carregar a classe na qual a chamada à Class.forName() está sendo realizada.

Quando uma dessas chamadas é realizada de forma explícita ocorre o seguinte: se a classe referenciada já estiver sido carregada anteriormente, uma referência à esta classe é retornada. Caso contrário o CL utiliza o modelo de delegação para carregar a classe.

Na segunda forma a classe é carregada por referência, ou seja, quando ela é referenciada por outra classe carregada em um momento distinto (conforme descrito no passo ‘Resolving‘ da fase ‘Linking‘ do processo de classloading descrito no tópico anterior). Assim como no carregamento implícito, caso a classe já tenha sido carregada, uma referência à ela é retornada. Caso contraário o carregamento realizado usando o modelo de delegação.

No modelo de delegação a implementação do classloader faz uma checagem inicial para verificar se a classe solicitada já foi carregada e reside no cache. Essa checagem melhora a performance de carregamento, pois caso a classe já esteja no cache, não será necessário carregá-la novamente do disco. Se a classe não for encontrada no cache o CL atual primeiro solicita a classe ao seu pai (superior imediato na hierarquia). Somente se a classe não puder ser carregada pelo CL pai o CL em questão (filho) tenta carregar a classe. Caso a classe solicitada seja encontrada nos dois CLs (pai e filho), a versão encontrada no CL pai é carregada.

Este modelo de delegação é utilizado para evitar que existam múltiplas cópias de uma mesma classe sejam carregadas. Múltiplas cópias de uma mesma classe podem ocosionar problemas de conflito de classes gerando exception como a famosa ClassCastException. Para mais detalhes sobre os diferentes problemas ocasionados devido ao conflito de classes em um classloader consulte o artigo “Demystifying class loading problems, Part 2: Basic class loading exceptions” [3]. Nesse artigo o autor explica as diferentes exceptions geralmente relaciodas ao conflito de classes em classloaders.

O ponto a ser destacado no modelo de delegação utilizado pelo Java Classloader é:

Um classloader filho (ex: application classloader) solicita a classe primeiro ao seu pai antes de tentar localizar e carregar por conta própria.

Por exemplo: Quando uma classe da sua aplicação (carregada pelo CL da aplicação) referencia uma classe fornecida pelo JDK (ex: java.lang.String), o carregamento é delegado pelo boostrap/root classloader (que de fato carregou as classes contidas em JAVA_HOME/jre/lib/rt.jar).

___
[1] http://docs.tpu.ru/docs/oracle/en/fmw/11.1.1.6.0/web.1111/e13706/classloading.htm
[2] http://www.ibm.com/developerworks/java/library/j-dclp1/
[3] http://www.ibm.com/developerworks/java/library/j-dclp2/

Anúncios
Dicas, Java, JVM, Tools

Colhendo envidências em caso de erro (crash) na JVM

Sabe aquela situação em que a equipe responsável pelo ambiente de produção chega pra você e diz: “o servidor X está caindo toda hora… já olhamos os recursos da máquina (rede, memória, cpu e disco), inclusive o log do servidor, mas não identificamos nada…”

Isso mesmo! Há casos em que seu processo caiu sem dar explicações e sem deixar pista alguma.

Já tive a oportunidade de passar por essa situação algumas vezes… Trata-se de uma situação complicada, pois quase sempre ocorre em um momento e ambiente crítico onde o tempo para solução é o seu pior inimigo. Bem, após bater cabeça com esse tipo de problema, posso afirmar que a melhor opção é preparar o seu ambiente para colher algumas evidências quando o problema voltar a ocorrer. Como quase tudo na TI esse tipo de situação é inevitável. Softwares e Hardware podem falhar por “n” motivos. Nesse post deixo algumas dicas que podem lhe ajudar na investigação “pós queda” de um processo Java (rodando em cima de uma JVM).

Existem dois casos em que é bastante comum o processo da JVM cair:

  1. insuficiência de memória
  2. algum erro fatal da jvm

O tipo de erro relacionado à insuficiência de memória é o conhecido por OutOfMemoryError (OOM) e ocorre devido à algum memory leak no código da aplicação. Para analisar a causa desse tipo de erro é interessante ter em mãos o estado (snapshot) do Heap da JVM no momento em que o erro ocorre. Para isso é necessário habilitar alguns parâmetros na JVM conforme abaixo:

Geração do Dump (formato hprof) do Heap da JVM.

Adicionar as seguintes propriedades no comando que inicia a JVM. Caso o comando seja parametrizado em um arquivo de configuração, basta usar uma variável JAVA_OPTS como no exemplo abaixo.

# HeapDUMP e Core DUMP
# registra a data e hora de início do processo
DATE_START=`date  +%d-%m-%Y-%k%M`

# Habilita a geração do DUMP de memória (HPROF)
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError "
JAVA_OPTS="$JAVA_OPTS -XX:OnOutOfMemoryError=<execute algum comando shell aqui (ex: 'kill -9%p')> "
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/var/log/jvm/heapdump_$DATE_START.hprof "

 

NOTA:

  • Recentemente encontrei uma implementação alternativa à opção ‘-XX:OnOutOfMemoryError’ interessante. Vale a pena testar!
  • o arquivo de dump no formato .hprof pode ser aberto com os utilizatários jVisualvm (fornecido pelo JDK Sun/Oracle Hotspot) ou Eclipse Memory Analizer (MAT). É necessário que a versão e arquitetura do JDK utilizado na análise dump seja idêntica à JVM utilizada pelo servidor de aplicação onde o erro ocorreu.

O segundo caso de erro pode ser gerado por algum problema entre a JVM e SO. Um caso típico é o erro de estouro de pilha conhecido como StackOverflowError. Esse tipo de erro pode ocorrer caso a aplicação utilize algum tipo de loop infinito ou uma recursividade muito profunda. O tamanho da pilha alocada pela JVM durante a criação de uma thread java é de 1024KB (1mb) em um sistema Linux x64. É possível alterar esse valor para mais ou para menos a depender da necessidade da aplicação. O parâmetro da JVM -Xss:<n>k altera o valor da pilha, sendo que <n> é um valor inteiro.

Geração do coredump (threads e memória) da JVM

Adicionar as seguintes propriedades no comando que inicia a JVM.

# Habilita a geração de coredump (BIN)
JAVA_OPTS="$JAVA_OPTS -XX:OnError='gcore -o /var/log/jvm/jboss_PID%p_$DATE_START.coredump %p' "
JAVA_OPTS="$JAVA_OPTS -XX:ErrorFile=/var/log/jvm/hs_err_jboss_PID%p_$DATE_START.log "

NOTAS:

  • A opção ‘-XX:OnError‘ é executada apenas quando  a causa raiz do erro for Nativo devido a alguma falha fora da JVM – JVM Native Crash.
  • o arquivo de dump no formato hprof pode ser aberto com os utilizatários jVisualvm (fornecido pelo JDK) ou Eclipse Memory Analizer (MAT).  É necessário que a versão e arquitetura do JDK utilizado na análise dump seja idêntica à JVM utilizada pelo servidor de aplicação onde o erro ocorreu.
  • Para que o comando gcore (invocado pela JVM após o evento de erro) funcione é necessário que o pacote gdb (A GNU source-level debugger for C, C++, Fortran and other languages) esteja devidamente instalado no Sistema Operacional.

Uma opção mais eficiente para geração de Dumps em JVMs com Heaps grandes (>= 2gb)

Utilizar o jmap em heap muito grandes pode demorar horas para concluir. Em algumas situações é necessário reiniciar o serviço imediatamente após a falha para diminuir o tempo em que ele fica fora do ar. Uma excelente alternativa é descrita nesse artigo disponível no Blog da Atlassian: So you want your JVM’s heap…

Gerando dumps da JVM em tempo de runtime manualmente

O formato binário pode ser aberto com as ferramentas: jstack (JDK), jVisualVM (JDK), Eclipse MAT ou qualquer outro profiler Java que reconheça o formato HPROF.

jmap -dump:live,format=b,file=jvm_heap.bin <PID>

O formato texto pode ser aberto com as ferramentas: jVisualVM (JDK), IBM Thread Analizer ou qualquer outro profiler Java que reconheça dump de threads Java.

jstack -F -l <PID> > /tmp/jvm_thread.dump

NOTA: os procedimentos acima causam o travamento das threads (equivalente ao efeito Stop The World do FullGC). Portanto utilize com cautela em ambiente de produção!

A análise dos dumps citados neste post é assunto para um novo post. Existem várias ferramentas que podem ser utilizadas na análise. Além das ferramentas é importante ter conhecimento da arquitertura da Máquina Virtual Java utilizada. No caso do dump de memória (heap) é importante conhecer a divisão geracional dos pools de memória da JVM , bem como os vários mecanismos de coleta de lixo. Para o dump de threads é importante conhecer um pouco sobre a execução de threads na plataforma Java: sincronização, pilha de execução, thread monitor, thread status (WAINTING, RUNNING, BLOCKED, etc). Com a ferramenta certa e com um pouco de paciência é possível chegar a tão desejada causa raiz do problema: nome da classe, nome do método, nome da biblioteca ou até mesmo a linha de código que causa ou influência o crash do processo Java.