
O artigo discutirá a implementação da compactação de ponteiro na Java Virtual Machine de 64 bits , que é controlada pela opção UseCompressedOops e é ativada por padrão para sistemas de 64 bits que começam com Java SE 6u23.
Descrição do problema
Em uma JVM de 64 bits, os ponteiros ocupam 2 vezes mais espaço de memória (surpresa-surpresa) do que em um de 32 bits. Isso pode aumentar o tamanho dos dados em 1,5 vezes em comparação com o mesmo código para a arquitetura de 32 bits. Ao mesmo tempo, na arquitetura de 32 bits, apenas 2 ^ 32 bytes (4 GB) podem ser endereçados, o que é bastante pequeno no mundo moderno.
Vamos escrever um pequeno programa e ver quantos bytes os objetos Inteiros ocupam:
import java.util.stream.IntStream; import java.util.stream.Stream; class HeapTest { public static void main(String ... args) throws Exception { Integer[] x = IntStream.range(0, 1_000_000).boxed().toArray(Integer[]::new); Thread.sleep(6000000); Stream.of(x).forEach(System.out::println); } }
Aqui destacamos um milhão de objetos Inteiros e adormecemos por um longo tempo. A última linha é necessária para que o compilador não ignore repentinamente a criação da matriz (embora, na minha máquina, os objetos sejam criados normalmente sem essa linha).
Compilamos e executamos o programa com compactação de ponteiro desativada:
> javac HeapTest.java > java -XX:-UseCompressedOops HeapTest
Usando o utilitário jcmd , examinamos a alocação de memória:
> jps 45236 HeapTest ... > jcmd 45236 GC.class_histogram

A imagem mostra que o número total de objetos é 1000128 e o tamanho da memória que esses objetos ocupam é 24003072 bytes . I.e. 24 bytes por objeto (por que exatamente 24 serão escritos abaixo).
E aqui está a memória do mesmo programa, mas com o sinalizador UseCompressedOops ativado :

Agora, cada objeto ocupa 16 bytes .
As vantagens da compactação são óbvias =)
Solução
Como a JVM compacta ponteiros? Essa técnica é chamada de Opa compactada . Oop significa ponteiro de objeto comum ou ponteiro de objeto comum .
O truque é que, em um sistema de 64 bits, os dados na memória estão alinhados com a palavra da máquina, ou seja, 8 bytes cada. E o endereço sempre tem três bits zero no final.
Se você salvar o ponteiro deslocando o endereço em 3 bits para a direita (a operação é chamada de codificação ) e, antes de usar, alternar em 3 bits para a esquerda (respectivamente, decodificar ), poderá ajustar ponteiros de 32 bits com um tamanho de 35 bits , ou seja, Endereço de até 32 GB (2 ^ 35 bytes).
Se o tamanho da pilha do seu programa for superior a 32 GB, a compactação deixará de funcionar e todos os ponteiros terão 8 bytes de tamanho.
Quando a opção UseCompressedOops está ativada, os seguintes tipos de ponteiros são compactados:
- Campo de classe para cada objeto
- Objetos de campo de classe
- Elementos de uma matriz de objetos.
Os objetos da própria JVM nunca são compactados. Nesse caso, a compactação ocorre no nível da máquina virtual, e não no bytecode.
Leia mais sobre como colocar objetos na memória
Agora, vamos usar o utilitário jol (Java Object Layout) para examinar mais de perto a quantidade de memória que nosso Inteiro ocupa em diferentes JVMs:
> java -jar jol-cli-0.9-full.jar estimates java.lang.Integer ***** 32-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 8 (object header) N/A 8 4 int Integer.value N/A 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 4 int Integer.value N/A 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM, compressed references enabled: *************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ***** 64-bit VM, compressed references enabled, 16-byte align: ************ java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
A diferença entre "VM de 64 bits" e "VM de 64 bits, referências compactadas ativadas" é reduzir o cabeçalho do objeto em 4 bytes. Além disso, no caso sem compactação, torna-se necessário adicionar mais 4 bytes para alinhar os dados na memória.
O que é esse cabeçalho de objeto? Por que diminuiu em 4 bytes?

A imagem mostra um cabeçalho de objeto de 12 bytes, ou seja, com a opção UseCompressedOops ativada. O cabeçalho consiste em alguns sinalizadores da JVM internos, bem como um ponteiro para a classe deste objeto. Pode-se ver que o ponteiro para a classe leva 32 bits. Sem compactação, ele ocuparia 64 bits e o tamanho do cabeçalho do objeto já seria de 16 bytes.
A propósito, você pode ver que há outra opção para o alinhamento de 16 bytes. Nesse caso, você pode aumentar a memória até 64 GB.
Contras Compressão de ponteiros
A compactação de ponteiros, é claro, tem um óbvio menos - o custo das operações de codificação e decodificação cada vez que o ponteiro é acessado. Os números exatos variam de acordo com a aplicação.
Por exemplo, aqui está um gráfico de pausa do coletor de lixo para ponteiros compactados e não compactados, extraídos daqui Java GC em Numbers - OOPs compactados

Pode-se observar que, com a compactação ativada, as pausas do GC duram mais. Você pode ler mais sobre isso no próprio artigo (o artigo é bastante antigo - 2013).
Referências:
Opa compactado na JVM do ponto de acesso
Como a JVM aloca objetos
CompressedOops: Introdução às referências compactadas em Java
Truque por trás de Oops compactados da JVM
Aprimoramentos de desempenho da máquina virtual Java HotSpot