Classes após compilação
Uma vez falando dos bytecodes é interessantes “puxar o gancho” e falar como fica uma classe após sua compilação. Como já foi dito após a compilação é gerado um bytecode, cujo arquivo possui a extensão .class, e cada arquivo representa apenas uma classe. Cada arquivo possui as seguintes características:
Um número mágico em hexadecimal definindo que essa clase é um .class o valor é 0xCAFEBABE
O maior e menor número da versão do arquivo class que juntos definem a versão do arquivo, ou seja, JVM antes rodar precisa verificar se a versão V que ela pode executar estar entre: Menor Versão<V< Maior Versão.
access_flags: Essa flag indica o tipo de encapsulamento da classe, métodos e de suas propriedades os flags podem ser:
- ACC_PUBLIC: flag método, atributos públicos
- ACC_PRIVATE: flag para privados
- ACC_PROTECTED: protected
- ACC_STATIC: estático
- ACC_FINAL: final
- ACC_SYNCHRONIZED: indica um método sincronizado
- ACC_BRIDGE: indica que o método foi gerado pelo compilador
- ACC_VARARGS: indica que é varags
- ACC_NATIVE: nativo
- ACC_ABSTRACT: abstrato
- ACC_STRICT: indica que o método é strict
- ACC_SYNTHETIC: indica que o método não é “original”
.
this_class: o valor da corrente classe deve ter um índice válido na constant pool
super_class: as informações da superclasse devem estar dentro e pode ocupar o índice zero ou não, se ocupar o índice zero essa classe é o java.lang.Object, a única classe que não possui pai, do contrário terá que ser um índice válido e ter informações que apontam para a classe pai. As informações da classe é definida pelo seu nome com o seu caminho, por exemplo, a nome da String seria java/lang/String.
constant pool: O constant pool é uma estrutura de tabela que contém o nome das classes, interfaces, métodos, atributos e outras informações das classes. Para guardar essas informações existem dois registadores par cada informação importante: O vetor coma s informações da classe, método, ou atributos e o seu contador ou índice que funciona como limitador do vetor. Esse é o caso das interfaces que a classe implementa, atributos, métodos que possuem seus respectivos vetor e índices.
As descrições dos atributos ou dos parâmetros em um método quanto ao seu tipo é definido a seguir:
- B byte signed byte
- C char
- D double
- F float
- I int
- J long
- L Classname ; referência
- S short
- Z boolean
- [ referência de um vetor
- [[ referência de uma matriz
Assim, por exemplo: double dobro(double d)
é igual (D)D
e Double dobro(Double d)
é (Ljava/lang/Double;)Ljava/lang/Double
.
Dentro da constant pool cada informação possui o seu primeiro byte que indica o seu tipo de informação:
- CONSTANT_Class 7
- CONSTANT_Fieldref 9
- CONSTANT_Methodref 10
- CONSTANT_InterfaceMethodref 11
- CONSTANT_String 8
- CONSTANT_Integer 3
- CONSTANT_Float 4
- CONSTANT_Long 5
- CONSTANT_Double 6
- CONSTANT_NameAndType 12
- CONSTANT_Utf8 1
- CONSTANT_MethodHandle 15
- CONSTANT_MethodType 16
- CONSTANT_InvokeDynamic 18
StackMapTable: é composto de stackmapframe e tem o objetivo de verificações para o bytecode
Para auxiliar a depuração na linguagem Java existem algumas informações para depurar o código essas variáveis são: LocalVariableTable e LocalVariableTypeTable que define as informações das variáveis locais para o debug e LineNumberTable define a parte do bytecode e sua correspondente linha de código.
Para as anotações exitem: RuntimeVisibleAnnotations
, RuntimeInvisibleAnnotations
, RuntimeVisibleParameterAnnotations
, RuntimeInvisibleParameterAnnotations
que contém informações das anotações quanto a sua visibilidade em tempo de execução aos atributos e métodos ou não, existem essas mesmas informações para os parâmetros quanto as suas visibilidades. AnnotationDefault
define as informações dentro das anotações.
O contador da consant pool possui 16 bits, ou seja, ele só pode conter 2¹⁶=65535 elementos, esse mesmo número vale para o número de métodos, atributos, interfaces implementadas, variáveis locais, pilha de operações (sendo que para esses dois últimos o longo e o double ocupam dois espaços), o nome do método ou atributo. O número de dimensões de uma matriz é 255 o mesmo número vale para a quantidade de parâmetros, caso não seja um método estático deve-se incluir a instância.
Com o objetivo de pôr em prática e visualizar esses bytescodes, será demonstrado um simples código e o seu respectivo bytecode.
public class PrimeiroTeste{
public Double somarInstancias(Double a, Double b) {
Double resultado = a + b;
return resultado;
}
public double somarDouble(double a, double b) {
return a + b;
}
public int somarInteiros(int a, int b) {
return a + b;
}
public short somarShort(short a, byte b) {
return (short) (a + b);
}
public static int somarStatic(int a, int b) {
return a + b;
}
}
Criado o arquivo PrimeiroTeste.java e inserido o código 1 nesse arquivo, os próximos passos serão entrar pelo terminal no caminho que se encontra o arquivo PrimeiroTeste.java, compilar e analisar o seu respectivo byte code com os seguintes comandos.
javac PrimeiroTeste.java
javap -verbose PrimeiroTeste
O resultado:
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."":()V
#2 = Methodref #22.#23 // java/lang/Double.doubleValue:()D
#3 = Methodref #22.#24 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
#4 = Class #25 // PrimeiroTeste
#5 = Class #26 // java/lang/Object
#6 = Utf8
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 somarInstancias
#11 = Utf8 (Ljava/lang/Double;Ljava/lang/Double;)Ljava/lang/Double;
#12 = Utf8 somarDouble
#13 = Utf8 (DD)D
#14 = Utf8 somarInteiros
#15 = Utf8 (II)I
#16 = Utf8 somarShort
#17 = Utf8 (SB)S
#18 = Utf8 somarStatic
#19 = Utf8 SourceFile
#20 = Utf8 PrimeiroTeste.java
#21 = NameAndType #6:#7 // "":()V
#22 = Class #27 // java/lang/Double
#23 = NameAndType #28:#29 // doubleValue:()D
#24 = NameAndType #30:#31 // valueOf:(D)Ljava/lang/Double;
#25 = Utf8 PrimeiroTeste
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/Double
#28 = Utf8 doubleValue
#29 = Utf8 ()D
#30 = Utf8 valueOf
#31 = Utf8 (D)Ljava/lang/Double;
Nesse primeiro resultado podemos visualizar a constant pool e a menor e a maior versão do class. O Constant Pool contém as informações da respectiva classe, já que toda classe possui essa informação, inclusive as InnerClasses, e eles são armazenadas em um vetor.
public PrimeiroTeste();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LPrimeiroTeste;
public java.lang.Double somarInstancias(java.lang.Double, java.lang.Double);
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=3
0: aload_1
1: invokevirtual #2 // Method java/lang/Double.doubleValue:()D
4: aload_2
5: invokevirtual #2 // Method java/lang/Double.doubleValue:()D
8: dadd
9: invokestatic #3 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
12: astore_3
13: aload_3
14: areturn
LineNumberTable:
line 5: 0
line 6: 13
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this LPrimeiroTeste;
0 15 1 a Ljava/lang/Double;
0 15 2 b Ljava/lang/Double;
13 2 3 resultado Ljava/lang/Double;
public double somarDouble(double, double);
flags: ACC_PUBLIC
Code:
stack=4, locals=5, args_size=3
0: dload_1
1: dload_3
2: dadd
3: dreturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LPrimeiroTeste;
0 4 1 a D
0 4 3 b D
public int somarInteiros(int, int);
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LPrimeiroTeste;
0 4 1 a I
0 4 2 b I
public short somarShort(short, byte);
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: i2s
4: ireturn
LineNumberTable:
line 17: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LPrimeiroTeste;
0 5 1 a S
0 5 2 b B
public static int somarStatic(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
LineNumberTable:
line 21: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 a I
0 4 1 b I
Ao vermos os métodos podemos perceber que todos os métodos possui o tamanho da pilha de operação e de variáveis além do tamanho das variáveis envolvidas em um determinado método.
No primeiro que é o método construtor, esse método é construído automaticamente caso não seja feita pelo usuário, ele possui 1 tamanho de operação, já que se trata da criação de um objeto do tipo referência e esse ocupa um espaço no vetor de operação, 1 no tamanho de variável, já que ele é um método não estático e essas informações pertence a interface que a está chamando e o número de variáveis utilizadas é de um já que estamos nos referindo a instância criada.
No segundo método, a soma de instâncias e retorna uma terceira instância, (Ljava/lang/Double;Ljava/lang/Double;)Ljava/lang/Double
, ele possui o tamanho de três na pilha de variáveis, já que uma para as duas variáveis de referência e uma já que o método é da instância, quatro no tamanho de pilha de operações, já que no processo os Doubles serão transformados em double primitivo, assim cada um ocupará duas unidades no vetor. O campo LineNumberTable
é para ajudar a debugar o código num determinado método, LineNumberTable
5: 0, diz que a linha de número cinco do código java equivale a instrução do começo, linha zero do bytecode e line 6: 13, a linha seis do código java começa na instrução do bytecode número 13. O campo LocalVariableTable
também serve para debugar e define o nome do campo, tipo, linha que ele nasce e a que morre. Isso demonstra como é diferente o código Java e o byteCode gerado.
Nesse capítulo falamos um pouco sobre o bytecode e o seu macete para entender, olhando pelas inicias do comando, vale lembrar que durante a execução os boolianos, byte, shorts são transformados para inteiros, assim suas operações são realizadas como inteiros. Se demonstrou quão diferente é o código Java do bytecode gerado e em função disso se criou tabelas e variáveis, por exemplo, LocalVariable e LineNumberTable para auxiliar na hora de debugar, essas variáveis são utilizadas pelo modo debug das IDEs modernas.