Marcos Roriz $:

Pessoal, Geek, Anime, Jogos, Ciência da Computação e Open Source…

ClassLoader desmitificado.

with 2 comments

Até recentemente não dava muita importância no funcionamento da estrutura interna da plataforma Java até quando tive problemas que o Google não conhece. Para resolver meus problemas tive que mergulhar a fundo no interior da JVM. E apesar de ter vários materiais na rede é complicado compreender o linguajar e o funcionamento interno da mesma. Para começar entender o funcionamento do seu programa dentro da Máquina Virtual Java (Java Virtual Machine – JVM) é necessário compreender o coração da JVM  o ClassLoader (CL). De maneira simples, um CL é uma estrutura que tem como responsabilidade o carregamento dos seus bytecodes (.class). Cada ClassLoader contém o que ela pode carregar no seu classpath específico, isso é, o endereço (diretórios) das classes. Um classpath pode conter vários diretórios, sendo assim o CL irá procurar a classe no primeiro diretório especificado, se não achar irá pro segundo, assim suscetivamente até acabar a lista de diretórios do classpath. Porém o mais importante é entender como o CL carrega as suas classes e como ele é organizado internamente.

Antes de mais nada é necessário explicar que um tipo (classe) em java é identificado pelo seu nome e seu CL. Ou seja um objeto da classe Folha de CL 0x2bbd86 é diferente de um objeto da classe Folha de CL 0x813b2 pois apesar de serem da mesma classe esses possuem CL diferentes. O java faz isso para garantir a consistência de objetos internamente na JVM. Pois caso contrário poderia ocorrer um conflito entre um objeto Folha que representa uma árvore e sua atribuição a um objeto Folha que representa uma folha de pagamento. Ou seja, objetos originados de uma classe de mesmo nome pode não possuir estruturas idênticas.

Também é importante entender o processo chave de carregamento de classe, a delegação. Um ClassLoader possui ClassLoader pais e sempre irá fazer o seguinte:

  • A Classe sempre irá pedir a ClassLoader que o carregou para carregar suas dependências;
  • ClassLoader sempre irá delegar o carregamento da classe ao seu pai, para verificar se o mesmo quer carrregar a classe antes.

Agora iremos explicar a hierarquia dos ClassLoaders. Quando a JVM inicia ela não possui nenhuma classe carregada. A JVM usa o mecanismo de carregamento de classes conhecido como lazyness, preguiçoso, a JVM vai carregando as classes conforme é necessário. Porém existe um problema, o código que carrega uma classe é o ClassLoader que em si é uma classe em Java, se a JVM não possui nenhuma classe carregada como é que ela vai carregar as outras classes pois conforme a especificação Java uma classe só é carregada quando todas as suas dependências forem carregadas. Conforme novamente a especificação Java, ClassLoader herda de Object que em si deve ser carregado por uma ClassLoader o que leva a um dead-lock. A solução para isso é ter um ClassLoader que de fato não é escrito em Java e sim em código nativo. Esse CL nativo irá carregar as classes ClassLoader, AppClassLoader e ExtClassLoader. A árvore de CL é a hierarquia formada pelo agrupamento dos mesmos. Por padrão temos a seguinte hierarquia:

Hierarquia de ClassLoaders

O BootStrap é o CL nativo executado pela JVM o seu classpath encontra as classes do sistema, principalmente o runtime do Java. O ExtCL (Extension ClassLoader) tem como seu classpath frameworks ou aplicativos específicos do usuário, caso queira que esses sejam visíveis em toda a VM. O pai do ExtCL é o BootStrap. E por fim o AppClassLoader que é o CL que irá carregar sua aplicação e seu classpath é o especificado no manifest do jar ou o passado na linha de comando, e o pai desse é o ExtCL.

Faremos uma aplicação para demonstrar os conceitos vistos:

public class Info {
	public static void main(String args[]) {
		System.out.println("Boot class path: " + System.getProperty("sun.boot.class.path"));
		System.out.println("Extension class path: " + System.getProperty("java.ext.dirs"));
		System.out.println("AppClassPath: " + System.getProperty("java.class.path"));

		Info i = new Info();
		System.out.println("\nBoot CL: " + java.lang.Object.class.getClassLoader());
		System.out.println("App ClassLoader: " + i.getClass().getClassLoader());
	}
}

Resultado obtido:

[marcos@hades AUR]$ java Info
Boot class path: /opt/java/jre/lib/resources.jar:/opt/java/jre/lib/rt.jar:/opt/java/jre/lib/sunrsasign.jar:/opt/java/jre/lib/jsse.jar:/opt/java/jre/lib/jce.jar:/opt/java/jre/lib/charsets.jar:/opt/java/jre/classes
Extension class path: /opt/java/jre/lib/ext:/usr/java/packages/lib/ext
AppClassPath: .

Boot CL: null
App ClassLoader: sun.misc.Launcher$AppClassLoader@19134f4

Desse resultado podemos claramente ver o que foi explicado, o classpath do BootStrap é o “SDK” do Java, o do ExtClassLoader é o diretório de extensões e o da AppClassLoader é o passado no argumento na cli, caso não tenha sido passado ele assume que é o diretório local: ., ou lido do manifest do jar. E também vemos que quem carregou nossa classe principal foi o AppClassLoader. Também vemos que o bootstrap CL é representado por null.

Bem você já deve ter adivinhado por agora que somente esses três CL são pré-definidos sendo assim nós podemos adicionar e alterar classloaders nessa árvore. Isso garante flexibilidade e isolamento de código, algo muito utilizado em Servidores de Aplicação como o JBoss e Glassfish. O servidor vai isolar cada requisição criando classloader de modo que uma requisição não seja visível na “árvore” a outra, isso garante uma robustez e segurança muito grande a plataforma.

Outro grande exemplo da manipulação de classloaders é o escopo estático de uma variável. Uma variável tem seu escopo estático, isso é, visibilidade, do CL  que foi carregado pra baixo. Vejamos um exemplo abaixo:

import java.io.*;
import java.net.*;
public class EscopoEstatico {
	public static int instanceCount = 0;
	public EscopoEstatico() {
		System.out.println("Valor da instancia:" + ++instanceCount);
	}

	public static void main (String args[]) throws Exception {
		new EscopoEstatico();
		// Vamos criar um Cl novo e colocalo abaixo do bootstrap
		// Consequentemente a variavel static vai ser diferente lá
		// do que a especificada.
		URL urlArray = {new File(System.getProperty("user.dir")).toURL() };
		URLClassLoader cl = new URLClassLoader(urlArray, null);
		cl.loadClass("EscopoEstatico").newInstance();
	}
}

Resultado:

[marcos@hades AUR]$ java EscopoEstatico
Valor da instancia:1
Valor da instancia:1

O que fizemos pode ser visualizado na imagem abaixo:
O problema é que a variável estática não está definida no caminho definido pelo o outro CL, então é considerado um novo “escopo”.

Então um dos problemas que pode surgir é, dado que eu carrego uma requisição em classloaders diferente e de modo que os mesmos não tenham nenhum CL em Comum, todos sejam filhos de null como garantir a visibilidade dó código ao longo da JVM? Bem o que sabemos é que todos diretamente ou indiretamente tem um CL em comum, o CL bootstrap (null). Porém esse CL só carrega os runtimes da plataforma Java, por padrão. Poucos sabem que podemos editar, adicionar, sobrescrever o classpath do BootStrap. Você pode até mesmo sobrescrever a implementação de uma classe que não gosta do JDK, como a implementação da pilha, etc. Concluindo, a ideia é estender os jars visíveis a este adicionando os arquivos que você queria que seja visível na JVM inteira. Isso é feito utilizando as opções X (extended) da JVM.

[marcos@hades AUR]$ java -X
    -Xmixed           mixed mode execution (default)
    -Xint             interpreted mode execution only
    -Xbootclasspath:<directories and zip/jar files separated by :>
                      set search path for bootstrap classes and resources
    -Xbootclasspath/a:<directories and zip/jar files separated by :>
                      append to end of bootstrap class path
    -Xbootclasspath/p:<directories and zip/jar files separated by :>
.......

Como disse anteriormente você pode sobrescrever (-Xbootclasspath) ou adicionar no começo (/p de prefix) ou no fim (/a de append) jars ao classpath do Bootstrap. Você pode até mesmo sobrescrever classes do JDK. Vejamos um exemplo abaixo:

Copie o arquivo Integer.java do source do jdk (java/lang) e modifique seu construtor
Arquivo Integer.java:

public class Integer {
...
    public Integer(int value) {
		System.out.println("Vou dividir seu int por 2, pq sou mau =P");
		System.out.println("VALOR ANTES: " + value + " VALOR DEPOIS " + value/2 + "\n");
		this.value = value / 2;
    }
....
}

Crie um diretório temporário, como ~/temp e também crie ~/temp/java/lang. Note que ~ representa o diretório home do usuário.
Compile o arquivo Integer.java e copie o seus .class’s para o seguinte diretório, ~/temp/java/lang.
Agora vamos fazer um simples teste.
Arquivo Teste.java:

public class Teste {
	public static void main(String args[]) {
		System.out.println(System.getProperty("sun.boot.class.path"));
		new Integer(33);
	}
}

Compile e coloque esse arquivo no diretório ~/temp.
Agora execute a seguinte linha de comando:

[marcos@hades AUR]$ java -Xbootclasspath/p:. Teste
Vou dividir seu int por 2, pq sou mau =P
VALOR ANTES: 1 VALOR DEPOIS 0

Vou dividir seu int por 2, pq sou mau =P
VALOR ANTES: 2 VALOR DEPOIS 1

Vou dividir seu int por 2, pq sou mau =P
VALOR ANTES: 15 VALOR DEPOIS 7

.:/opt/java/jre/lib/resources.jar:/opt/java/jre/lib/rt.jar:/opt/java/jre/lib/sunrsasign.jar:/opt/java/jre/lib/jsse.jar:/opt/java/jre/lib/jce.jar:/opt/java/jre/lib/charsets.jar:/opt/java/jre/classes
Vou dividir seu int por 2, pq sou mau =P
VALOR ANTES: 33 VALOR DEPOIS 16

Vamos chamar agora sem modificar o classpath do bootstrap:

[marcos@hades AUR]$ java Teste
/opt/java/jre/lib/resources.jar:/opt/java/jre/lib/rt.jar:/opt/java/jre/lib/sunrsasign.jar:/opt/java/jre/lib/jsse.jar:/opt/java/jre/lib/jce.jar:/opt/java/jre/lib/charsets.jar:/opt/java/jre/classes

A própria JVM quando inicia executa algumas chamadas criando novos Integers, como podemos ver. Porém o grande resultado é que vemos que o diretório atual, representado por ., foi pré-fixado na classpath do BootStrap e não foi chamado o Integer normal do JDK. Isso garante uma flexibilidade enorme, pois podemos facilmente alterar qualquer implementação não desejada do JDK. Alguns hackers modificavam versões da classe System pois o método getEnv de pegar o PATH do usuário foi depreciado antigamente e não era possível chama-lo até recentemente (acho que adicionaram denovo no jdk 1.5-1.6). Porém a ideia principal é que podemos facilmente garantir uma classe escopo global na JVM colocando seu jar ou .class na lista de classpath do BootStrap.

No próximo tópico irei postar algumas modificações, manipulações e resultados de brincar com os classloaders.

Referências:
[1] Java Geek, Using the BootClasspath – http://www.javageeks.com/~tneward
[2] Inside the Java Virtual Machine – Bill Vernes – The McGraw-Hill Companies (1997)
[3] Inside Java™ 2 Platform Security: Architecture, API Design, and Implementation, Second Edition By Li Gong, Gary Ellison, Mary Dageforde

About these ads

Written by marcosroriz

December 16, 2009 at 8:00 pm

Posted in Ciência da Computação, Java

Tagged with ,

2 Responses

Subscribe to comments with RSS.

  1. Olá Marcos,

    Primeiramente, parabéns por este ótimo post, muito bem explicado. Alias me fez lembrar uma coisa, à alguns anos atrás fiz uma entrevista para uma grande empresa de fora do Brasil e uma das perguntas dele foi justamente sobre ClassLoader, lembro-me que a pergunta era mais ou menos assim: “Quantos ClassLoader tem o Tomcat?” (Padrão sem adicionar novos). Bom, na hora não respondi porque não sabia, logo depois da entrevista corri para o PC para ver a resposta.
    Mas isso tudo é só p/ exemplificar a importância de ter um conhecimento mais profundo sobre a ferramente/linguagem que vc usa no seu cotidiano.

    Mais uma vez parabéns!

    Marivaldo Cabral

    December 29, 2009 at 11:10 am

    • Valeu :)
      Sabia que meus posts um dia iriam ser lidos LOL.

      E sim, e bem legal aprender como funciona a ferramenta internamente.

      Marcos Roriz

      December 30, 2009 at 10:11 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: