Do Código Java ao Heap Java

Entendendo e otimizando o uso de memória do seu aplicativo

Este artigo oferece um insight sobre o uso da memória do código Java™ , cobrindo a sobrecarga de memória obtida ao colocar um valor int em um objeto Integer o custo da delegação do objeto e a eficiência da memória dos diferentes tipos de coleção. Você aprenderá a determinar onde ocorrem as ineficiências em seu aplicativo e como escolher a coleção correta para aprimorar seu código.

Chris Bailey, Java Service Architect, IBM

Chris BaileyChris Bailey é membro da equipe IBM Java Technology Center no Hursley Park Development Lab no Reino Unido. Como arquiteto técnico para a organização de serviço e suporte IBM Java, ele é responsável por permitir que os usuários do SDK IBM para Java proporcionem implementações de aplicativo bem-sucedidas. Chris também está envolvido na reunião e avaliação de novos requisitos, na entrega de novos recursos e ferramentas de depuração, melhorias em documentação e na qualidade geral do SDK IBM para Java.



14/Mai/2014

Embora o assunto de otimização do uso da memória do código de seu aplicativo não seja novo, não é um tema geralmente bem entendido. Este artigo abrange brevemente o uso da memória de um processo Java e, em seguida, se aprofunda no uso da memória do código Java escrito por você. Finalmente, ele mostra maneiras de tornar o código de seu aplicativo mais eficiente com relação à memória, particularmente na área relacionada ao uso de coleções Java como HashMaps e ArrayLists.

Segundo plano: uso da memória de um processo Java

Inteligência nativa

Para obter uma compreensão mais detalhada do uso da memória de processo de um aplicativo Java, leia os artigos de Andrew Hall, "Thanks for the memory", no developerWorks. Eles cobrem o layout e o espaço de usuário disponível no Windows® e Linux® e no AIX®, e a interação entre o heap Java e o heap nativo.

Quando você executa um aplicativo Java executando java na linha de comando ou iniciando algum middleware baseado em Java, o Java Runtime cria um processo do sistema operacional — como se você estivesse executando um programa baseado em C. Na verdade, a maioria das JVMs é escrita em C ou C++. Como um processo do sistema operacional, o Java runtime encara as mesmas restrições com relação à memória que qualquer outro processo: a endereçabilidade fornecida pela arquitetura e o espaço do usuário fornecido pelo sistema operacional.

A endereçabilidade de memória fornecida pela arquitetura depende do tamanho do bit do processador — por exemplo, 32 ou 64 bits, ou 31 bits no caso do mainframe. O número de bits com o qual o processo pode lidar determina a faixa da memória que o processador é capaz de endereçar: 32 bits fornecem uma faixa endereçável de 2^32, que é 4.294.967.296 bits ou 4GB. A faixa endereçável de um processador de 64 bits é significativamente maior: 2^64 representa 18.446.744.073.709.551.616 ou 16 exabytes.

Algumas das faixas endereçáveis fornecidas pela arquitetura do processador são usadas pelo próprio sistema operacional para seu kernel e (para JVMs escritas em C ou C++) para o tempo de execução de C. A quantidade de memória usada pelo sistema operacional e tempo de execução de C depende do sistema operacional que está sendo usado, mas é normalmente significativo: o uso padrão do Windows é de 2 GB. O espaço endereçável restante — chamado de espaço do usuário— é a memória disponível para o processo real em execução.

Para aplicativos Java, o espaço do usuário é a memória usada pelo processo Java, sendo composto efetivamente por dois pools: o(s) heap(s) Java e o heap nativo (não Java). O tamanho do heap Java é controlado pelas configurações do heap Java da JVM: -Xms e -Xmx definem o heap Java mínimo e máximo, respectivamente. O heap nativo é o espaço do usuário que restou após o heap Java ter sido alocado na configuração de tamanho máximo. A Figura 1 mostra um exemplo de qual pode ser a aparência para um processo Java de 32 bits:

Figura 1. Exemplo do layout da memória para um processo Java de 32 bits

Na Figura 1, o sistema operacional e o tempo de execução C usam cerca de 1 GB dos 4 GB de faixa endereçável, o heap Java usa quase 2GB e o heap nativo usa o restante. Observe que a própria JVM usa a memória — da mesma forma que o kernel do sistema operacional e o tempo de execução C usam — e que a memória usada pela JVM é um subconjunto do heap nativo.


Anatomia de um objeto Java

Quando seu código Java usa o operador new para criar uma instância de um objeto Java, muito mais dados são alocados do que você espera. Por exemplo, talvez você se surpreenda em saber que a proporção do tamanho de um valor int para um objeto Integer— o menor objeto que pode deter um valor int— é normalmente 1:4. A sobrecarga adicional são metadados usados pela JVM para descrever o objeto Java, nesse caso um objeto Integer.

A quantidade de metadados de objeto varia de acordo com a versão da JVM e do fornecedor, mas é normalmente composta por:

  • Class : um ponteiro para as informações sobre a classe, que descreve o tipo de objeto. No caso de um objeto java.lang.Integer , por exemplo, esse é um ponteiro para a classe java.lang.Integer .
  • Flags : uma coleção de sinalizadores que descreve o estado do objeto, incluindo o código hash para o objeto, se houver um, e a forma do objeto (ou seja, se o objeto é um array ou não).
  • Lock : as informações de sincronização para o objeto — ou seja, se o objeto está sincronizado atualmente.

Os metadados do objeto são, em seguida, seguidos pelos próprios dados do objeto, compostos pelos campos armazenados na instância do objeto. No caso de um objeto java.lang.Integer , é um único int.

Portanto, quando você cria uma instância de um objeto java.lang.Integer ao executar uma JVM de 32 bits, o layout do objeto pode parecer com a Figura 2:

Figura 2. Exemplo de layout de um objeto java.lang.Integer para um processo Java de 32 bits

Como mostra a Figura 2 , 128 bits de dados são usados para armazenar os 32 bits de dados no valor int , pois os metadados do objeto usam o restante desses 128 bits.


Anatomia de um objeto de array Java

A forma e estrutura de um objeto de array, como um array de valores int , são parecidos com os de um objeto Java padrão. A principal diferença é que o objeto de matriz tem uma parte adicional de metadados que indica o tamanho do array. Os metadados de um objeto de array são compostos por:

  • Class : um ponteiro para as informações sobre a classe, que descreve o tipo de objeto. No caso de um array de campos int , é um ponteiro para a classe int[] .
  • Flags : uma coleção de sinalizadores que descrevem o estado do objeto, incluindo o código hash para o objeto, caso ele tenha um, e a sua forma (ou seja, se o objeto é ou não um array).
  • Lock : as informações de sincronização para o objeto — ou seja, se o objeto está sincronizado atualmente.
  • Size : o tamanho do array.

A Figura 3 mostra um exemplo de layout para um objeto de array int :

Figura 3. Exemplo de layout de um objeto de array int para um processo Java de 32 bits

Na Figura 3, 160 bits de dados armazenam os 32 bits de dados no valor int , pois os metadados do array usam o restante desses 160 bits. Para primitivos como byte, int e long, um array de entrada única é mais dispendioso em termos de memória do que o objeto do wrapper correspondente (Byte, Integer ou Long) para o campo único.


Anatomia de estruturas de dados mais complexas

Um bom design e programação orientados ao objeto incentiva o uso de encapsulamento (fornecimento de classes de interface que controlam o acesso aos dados) e delegação (o uso de objetos auxiliares para execução de tarefas). O encapsulamento e a delegação fazem com que a representação da maioria das estruturas de dados envolva diversos objetos. Um exemplo simples é um objeto java.lang.String . Os dados em um objeto java.lang.String é um array de caracteres contidos em um objeto java.lang.String que gerencia e controla o acesso ao array de caracteres. O layout de um objeto java.lang.String para um processo Java de 32 bits pode parecer com a Figura 4:

Figura 4. Exemplo de layout de um objeto java.lang.String para um processo Java de 32 bits

Como mostra a Figura 4 , um objeto java.lang.String contém — além dos metadados de objeto padrão — alguns campos para gerenciar os dados da cadeia de caractere. Normalmente, esses campos são um valor do hash, uma contagem do tamanho da cadeia de caractere, a compensação nos dados da cadeia de caractere e uma referência de objeto para o próprio array de caracteres.

Isso significa que para ter uma cadeia de caractere com 8 caracteres (128 bits de dados char ), 256 bits de dados são para o array de caracteres e 224 bits de dados são para o objeto java.lang.String que a gerencia, totalizando 480 bits (60 bytes) para representar 128 (16 bytes) de dados. Essa é uma proporção de sobrecarga de 3.75:1.

Em geral, quanto mais complexa a estrutura de dados se torna, maior sua sobrecarga. Isso é discutido com mais detalhes na próxima seção.


Objetos Java de 32 bits e 64 bits

Os tamanhos e sobrecarga dos objetos nos exemplos anteriores se aplicam a um processo Java de 32 bits. Como você aprendeu na seção Segundo plano: uso da memória de um processo Java , um processador de 64 bits tem um nível muito mais alto de endereçabilidade de memória do que um processador de 32 bits. Com um processo de 64 bits, o tamanho de alguns dos campos de dados no objeto Java — especificamente, os metadados do objeto e qualquer campo que faça referência a outro objeto — também precisa aumentar para 64 bits. Os outros tipos de campo de dados — tais como int, byte e long — não mudam de tamanho. A Figura 5 mostra o layout para um objeto Integer de 64 bits e para um array int :

Figura 5. Exemplo de layout de um objeto java.lang.Integer e um array int para um processo Java de 64 bits

A Figura 5 mostra que para um objeto Integer de 64 bits, 224 bits de dados estão sendo usados para armazenar os 32 bits usados para o campo int . — uma proporção de sobrecarga de 7:1. Para um array de elemento único e 64 bits int , 288 bits de dados são usados para armazenar a entrada int de 32 bits — uma sobrecarga de 9:1. O efeito disso em aplicativos reais é que o uso da memória do heap Java de um aplicativo executado anteriormente em um Java runtime de 32 bits aumenta drasticamente quando é movido para um Java runtime de 64 bits. Normalmente, o aumento está na ordem de 70 por cento do tamanho original do heap. Por exemplo, um aplicativo Java usando 1 GB do heap Java com o Java runtime de 32 bits normalmente usará 1,7 GB de heap Java com o Java runtime de 64 bits.

Observe que esse aumento na memória não é limitado ao heap Java. O uso da área da memória do heap nativo também aumentará, às vezes em até 90 por cento.

A Tabela 1 mostra os tamanhos de campo para objetos e arrays quando um aplicativo é executado no modo 32 bits e 64 bits:

Tabela 1. Tamanhos de campo em objetos para Java runtimes de 32 bits e 64 bits

Tipo de campoTamanho do campo (bits)
ObjetoArray
32 bits64 bits32 bits64 bits
operadores booleanos323288
byte323288
char32321616
short32321616
int32323232
float32323232
long32326464
double32326464
Campos de objeto3264 (32*)3264 (32*)
Metadados de objeto3264 (32*)3264 (32*)

*O tamanho dos campos de objeto e dos dados usados para cada uma das entradas de metadados de objeto pode ser reduzido para 32 bits por meio das tecnologias Referências compactadas ou OOPs compactados .

Referências compactadas e Ordinary Object Pointers (OOPs) compactados

As JVMs IBM e Oracle fornecem recursos de compactação de referência do objeto por meio das opções de Referências compactadas (-Xcompressedrefs) e OOPs compactados (-XX:+UseCompressedOops), respectivamente. O uso dessas opções permite que os campos do objeto e os valores de metadados do objeto sejam armazenados em 32 bits em vez de 64 bits. Isso tem o efeito de negação do aumento de 70 por cento da memória heap Java quando um aplicativo é movido de um Java runtime de 32 bits para um Java runtime de 64 bits. Observe que as opções não têm efeito sobre o uso da memória do heap nativo. Ele ainda é superior com o Java runtime 64 bits do que com o Java runtime de 32 bits.


Uso da memória das coleções Java

Na maioria dos aplicativos, uma grande quantidade de dados é armazenada e gerenciada usando as classes padrão Java Collections fornecidas como parte da API Java base. Se a otimização da área de cobertura da memória for importante para seu aplicativo, será especialmente útil entender a função que cada coleção fornece e a sobrecarga de memória associada. Em geral, quanto maior for o nível de recursos funcionais de uma coleção, maior será sua sobrecarga de memória — portanto, usar os tipos de coleção que fornecem mais funções do que você precisa causará uma sobrecarga de memória adicional desnecessária.

Algumas das coleções usadas normalmente são:

Com a exceção de HashSet, essa lista está em ordem decrescente de função e sobrecarga de memória. (Uma HashSet, sendo um wrapper ao redor de um objeto HashMap , fornece de forma efetiva menos funções do que HashMap embora seja um pouco maior.)

Coleções Java: HashSet

Uma HashSet é uma implementação da interface Set . A documentação da API da Java Platform SE 6 descreve HashSet como:

Uma coleção que não contém elementos duplicados. Mais formalmente, os conjuntos não contêm um par de elementos e1 e e2 como e1.equals(e2), e no máximo um elemento nulo. Conforme fica implícito por seu nome, essa interface modela a abstração de conjunto matemática.

Uma HashSet tem menos recursos do que uma HashMap , pois não pode conter mais do que uma entrada nula e não pode ter entradas duplicadas. A implementação é um wrapper que envolve uma HashMap, com o objeto HashSet gerenciando o que é permitido ser inserido no objeto HashMap . A função adicional de restrição dos recursos de uma HashMap significa que HashSets têm uma sobrecarga de memória um pouco superior.

A Figura 6 mostra o layout e o uso da memória de uma HashSet em um Java runtime de 32 bits:

Figura 6. Uso da memória e layout de uma HashSet em um Java runtime de 32 bits

A Figura 6 mostra o heap superficial (uso da memória do objeto individual) em bytes, junto com o heap retido (uso da memória do objeto individual e seus objetos filho) em bytes para um objeto java.util.HashSet . O tamanho do heap superficial é de 16 bytes, e o tamanho do heap retido é de 144 bytes. Quando uma HashSet é criada, sua capacidade padrão — o número de entradas que podem ser colocadas no conjunto — é de 16 entradas. Quando uma HashSet é criada com a capacidade padrão e nenhuma entrada é colocada no conjunto, ela ocupa 144 bytes. Isso representa 16 bytes extras sobre o uso da memória de uma HashMap. A Tabela 2 mostra os atributos de uma HashSet:

Tabela 2. Atributos de uma HashSet

Capacidade padrão16 entradas
Tamanho vazio144 bytes
Sobrecarga16 bytes mais a sobrecarga de HashMap
Sobrecarga para uma coleção de 10K16 bytes mais a sobrecarga de HashMap
Desempenho de pesquisa/inserção/exclusãoO(1) — O tempo usado é um tempo constante, independentemente do número de elementos (presumindo a não existência de colisões de hash)

Coleções Java: HashMap

Uma HashMap é uma implementação da interface Map . A documentação da API da Java Platform SE 6 descreve HashMap como:

Um objeto que mapeia as chaves para os valores. Um mapa não pode conter chaves duplicadas; cada chave pode ser mapeada no máximo até um valor.

HashMap fornece uma forma de armazenar pares de chave/valor, usando uma função hash para transformar a chave em um índice, na coleção onde o par de chave/valor é armazenado. Isso permite um acesso rápido ao local dos dados. As entradas nulas e duplicadas são permitidas; como tal, uma HashMap é uma simplificação de uma HashSet.

A implementação de uma HashMap é um array de objetos HashMap$Entry . A Figura 7 mostra o uso da memória e o layout de uma HashMap em um Java runtime de 32 bits:

Figura 7. Uso da memória e layout de uma HashMap em um Java runtime de 32 bits

Como mostra a Figura 7 , quando uma HashMap é criada, o resultado é um objeto HashMap e um array de objetos HashMap$Entry com sua capacidade padrão de 16 entradas. Isso proporciona à HashMap um tamanho de 128 bytes quando está completamente vazia. Quaisquer pares de chave/valor inseridos na HashMap são envolvidos por um objeto HashMap$Entry , que possui alguma sobrecarga.

A maioria das implementações de objetos HashMap$Entry contém os seguintes campos:

  • int KeyHash
  • Object next
  • Object key
  • Object value

Um objeto HashMap$Entry de 32 bytes gerencia os pares de chave/valor de dados colocados na coleção. Isso significa que a sobrecarga total de uma HashMap é composta pelo objeto HashMap , uma entrada de array HashMap$Entry e um objeto HashMap$Entry para cada entrada. Isso pode ser expresso pela fórmula:

HashMap objeto + sobrecarga do objeto de array + (número de entradas * (HashMap$Entry entrada de array + objeto HashMap$Entry ))

Para um HashMap com 10.000 entradas, apenas a sobrecarga do array HashMap, HashMap$Entry e de objetos HashMap$Entry é aproximadamente 360K. Isso antes do tamanho das chaves e dos valores que está sendo armazenado ser levado em consideração.

A Tabela 3 mostra os atributos de HashMap:

Tabela 3. Atributos de uma HashMap

Capacidade padrão16 entradas
Tamanho vazio128 bytes
Sobrecarga64 bytes mais 36 bytes por entrada
Sobrecarga para uma coleção de 10K~360K
Desempenho de pesquisa/inserção/exclusãoO(1) — O tempo usado é um tempo constante, independentemente do número de elementos (presumindo a não existência de colisões de hash)

Coleções Java: Hashtable

Hashtable, como HashMap, é uma implementação da interface Map . A descrição da documentação da API da Java Platform SE 6 de Hashtable é:

Essa classe implementa um hashtable, que mapeia as chaves para os valores. Qualquer objeto não nulo pode ser usado como uma chave ou como um valor.

Hashtable é muito semelhante a HashMap, mas apresenta duas limitações. Não pode aceitar valores nulos para a chave ou as entradas de valor e é uma coleção sincronizada. Em contraste, HashMap pode aceitar valores nulos e não é sincronizada, mas pode ser sincronizada usando o método Collections.synchronizedMap() .

A implementação da Hashtable — também é semelhante a HashMap— é como um array de objetos de entrada, nesse caso objetos Hashtable$Entry . A Figura 8 mostra o uso da memória e o layout de uma Hashtable em um Java runtime de 32 bits:

Figura 8. Uso da memória e layout de uma Hashtable em um Java runtime de 32 bits

A Figura 8 mostra que quando uma Hashtable é criada, o resultado é um objeto Hashtable usando 40 bytes de memória junto com um array de Hashtable$entrys com uma capacidade padrão de 11 entradas, totalizando um tamanho de 104 bytes para um Hashtable vazio.

Hashtable$Entry armazena efetivamente os mesmos dados que HashMap:

  • int KeyHash
  • Object next
  • Object key
  • Object value

Isso significa que o objeto Hashtable$Entry também tem 32 bytes para a entrada de chave/valor na Hashtable e o cálculo para a sobrecarga e o tamanho de Hashtable de uma coleção de entrada de 10K (aproximadamente 360K) é semelhante ao da HashMap.

A Tabela 4 mostra os atributos de uma Hashtable:

Tabela 4. Atributos de uma Hashtable

Capacidade padrão11 entradas
Tamanho vazio104 bytes
Sobrecarga56 bytes mais 36 bytes por entrada
Sobrecarga para uma coleção de 10K~360K
Desempenho de pesquisa/inserção/exclusãoO(1) — O tempo usado é um tempo constante, independentemente do número de elementos (presumindo a não existência de colisões de hash)

Como se pode ver, Hashtable tem uma capacidade um pouco menor do que HashMap (11 versus 16). Caso contrário, as principais diferenças são a incapacidade de Hashtable de aceitar chaves e valores nulos e sua sincronização padrão, o que talvez não seja necessário e reduza o desempenho da coleção.

Coleções Java: LinkedList

Uma LinkedList é uma implementação de lista vinculada da interface List . A documentação da API da Java Platform SE 6 descreve LinkedList como:

Uma coleção ordenada (também conhecida como uma sequência). O usuário dessa interface tem um controle preciso sobre onde cada elemento está inserido na lista. O usuário pode acessar elementos por seus índices de número inteiro (posição na lista) e pesquisar por elementos na lista. Ao contrário dos conjuntos, as listas normalmente permitem elementos duplicados.

A implementação de uma lista vinculada de objetos LinkedList$Entry . A Figura 9 mostra o uso da memória e o layout de LinkedList em um Java runtime de 32 bits:

Figura 9. Uso da memória e layout de uma LinkedList em um Java runtime de 32 bits

A Figura 9 mostra que quando uma LinkedList é criada, o resultado é um objeto LinkedList usando 24 bytes de memória junto com um único objeto LinkedList$Entry , totalizando 48 bytes de memória para uma LinkedList.

Uma das vantagens das listas vinculadas é que elas têm o tamanho preciso e não precisam ser redimensionadas. A capacidade padrão é efetivamente uma entrada e isso cresce e diminui dinamicamente à medida que mais entradas são adicionadas ou removidas. Ainda há uma sobrecarga para cada objeto LinkedList$Entry , cujos campos de dados são:

  • Object previous
  • Object next
  • Object value

Porém, isso é menor do que a sobrecarga de HashMaps e Hashtables, pois as listas vinculadas armazenam somente uma única entrada em vez de um par de chave/valor, e não há a necessidade de armazenar um valor do hash, pois as consultas baseadas em array não são usadas. No lado negativo, as consultas em uma lista vinculada podem ser muito mais lentas, pois a lista vinculada precisa ser atravessada para que a entrada correta seja encontrada. Para listas vinculadas maiores, isso pode resultar em longos tempos de consulta.

A Tabela 5 mostra os atributos de uma LinkedList:

Tabela 5. Atributos de uma LinkedList

Capacidade padrão1 entrada
Tamanho vazio48 bytes
Sobrecarga24 bytes, mais 24 bytes por entrada
Sobrecarga para uma coleção de 10K~240K
Desempenho de pesquisa/inserção/exclusãoO(n) — O tempo decorrido é linearmente dependente do número de elementos

Coleções Java: ArrayList

A ArrayList é uma implementação de array redimensionável da interface List . A documentação da API da Java Platform SE 6 descreve ArrayList como:

Uma coleção ordenada (também conhecida como uma sequência). O usuário dessa interface tem um controle preciso sobre onde cada elemento está inserido na lista. O usuário pode acessar elementos por seus índices de número inteiro (posição na lista) e pesquisar por elementos na lista. Ao contrário dos conjuntos, as listas normalmente permitem elementos duplicados.

Ao contrário de LinkedList, ArrayList é implementada usando um array de Objects. A Figura 10 mostra o uso da memória e o layout de uma ArrayList em um Java runtime de 32 bits:

Figura 10. Uso da memória e layout de uma ArrayList em um Java runtime de 32 bits

A Figura 10 mostra que quando uma ArrayList é criada, o resultado é um objeto ArrayList usando 32 bytes de memória, junto com um array Object com um tamanho padrão de 10, totalizando 88 bytes de memória para uma ArrayList vazia Isso significa que o objeto ArrayList não tem o tamanho preciso e, portanto, tem uma capacidade padrão, que, por acaso, é de 10 entradas.

A Tabela 6 mostra atributos de uma ArrayList:

Tabela 6. Atributos de uma ArrayList

Capacidade padrão10
Tamanho vazio88 bytes
Sobrecarga48 bytes mais 4 por entrada
Sobrecarga para uma coleção de 10K~40K
Desempenho de pesquisa/inserção/exclusãoO(n) — O tempo decorrido é linearmente dependente do número de elementos

Outros tipos de "coleções"

Além das coleções padrão, StringBuffer também pode ser considerada uma coleção, pois gerencia os dados de caractere e é parecida em estrutura e recursos com outras coleções. A documentação da API da Java Platform SE 6 descreve StringBuffer como:

Uma sequência thread-safe e mutável de caracteres.... O buffer de cada cadeia de caractere tem uma capacidade. Contanto que o tamanho da sequência de caractere contida no buffer da cadeia de caractere não exceda a capacidade, não será necessário alocar um novo array de buffer interno. Se houver estouro do buffer interno , ele será automaticamente aumentado.

A implementação de uma StringBuffer é um array de objetos chars. A Figura 11 mostra o uso da memória e o layout de uma StringBuffer em um Java runtime de 32 bits:

Figura 11. Uso da memória e layout de uma StringBuffer em um Java runtime de 32 bits

A Figura 11 mostra que quando uma StringBuffer é criada, o resultado é um objeto StringBuffer usando 24 bytes de memória, junto com um array de caracteres com um tamanho padrão de 16, totalizando 72 bytes de dados para uma StringBuffer vazia.

Assim como as coleções, StringBuffer tem uma capacidade padrão e um mecanismo para redimensionamento. A Tabela 7 mostra os atributos de StringBuffer:

Tabela 7. Atributos de um StringBuffer

Capacidade padrão16
Tamanho vazio72 bytes
Sobrecarga24 bytes
Sobrecarga para uma coleção de 10K24 bytes
Desempenho de pesquisa/inserção/exclusãoNão disponível

Espaço vazio em coleções

A sobrecarga das diversas coleções com um determinado número de objetos não resume toda a história de sobrecarga da memória. As medições nos exemplos anteriores assumem que as coleções foram precisamente dimensionadas. Porém, para a maioria das coleções, isso provavelmente não é verdade. A maioria das coleções é criada com uma capacidade inicial determinada, e os dados são colocados na coleção. Isso significa que é comum para as coleções ter uma capacidade superior a dos dados que estão sendo armazenados na coleção, o que apresenta uma sobrecarga adicional.

Considere o exemplo de uma StringBuffer. Sua capacidade padrão é de 16 entradas de caractere com um tamanho de 72 bytes. Inicialmente, nenhum dado está sendo armazenado nos 72 bytes. Se você colocar alguns caracteres no array de caracteres — , por exemplo, "MY STRING" — , você estará armazenando 9 caracteres no array de 16 caracteres. A Figura 12 mostra o uso da memória de uma StringBuffer contendo "MY STRING" em um Java runtime de 32 bits:

Figura 12. Uso da memória de uma StringBuffer contendo "MY STRING" em um Java runtime de 32 bits

Como mostra a Figura 12 , 7 entradas de caractere adicionais disponíveis no array não estão sendo usadas, mas estão consumindo memória— nesse caso uma sobrecarga adicional de 112 bytes. Para essa coleção, você tem 9 entradas em uma capacidade 16, o que proporciona uma proporção de preenchimento de0.56. Quanto menor a proporção de preenchimento da uma coleção, maior será a sobrecarga devida à capacidade sobressalente.


Expansão e redimensionamento das coleções

Assim que uma coleção alcança sua capacidade e uma solicitação é feita para colocar entradas adicionais na coleção, a coleção é redimensionada e expandida para acomodar novas entradas. Isso aumenta a capacidade, mas frequentemente diminui a proporção de preenchimento e apresenta uma sobrecarga de memória maior.

O algoritmo de expansão usado é diferente entre as coleções, mas uma abordagem comum é duplicar a capacidade da coleção. Essa é a abordagem usada para StringBuffer. No caso do exemplo anterior de StringBuffer, se você quisesse anexar " OF TEXT" ao buffer para produzir "MY STRING OF TEXT", é necessário expandir a coleção, pois sua nova coleção de caracteres tem 17 entradas contra uma capacidade atual de 16. A Figura 13 mostra o uso resultante da memória:

Figura 13. Uso da memória de uma StringBuffer contendo "MY STRING OF TEXT" em um Java runtime de 32 bits

Agora, como mostra a Figura 13 , você tem um array de caracteres de 32 entradas e 17 entradas usadas, obtendo uma proporção de preenchimento de 0.53. A proporção de preenchimento não caiu drasticamente, mas agora você tem uma sobrecarga de 240 bytes para a capacidade sobressalente.

No caso de cadeia de caractere e coleções pequenas, as sobrecargas para proporções de preenchimento e capacidade sobressalente baixas talvez não pareçam um problema, mas elas se tornam muito mais aparentes e dispendiosas em tamanhos superiores. Por exemplo, se você criar uma StringBuffer que contém apenas 16 MB de dados, ela usará (por padrão) um array de caracteres dimensionado para acomodar até 32 MB de dados — criando 16 MB de sobrecarga adicional na forma de capacidade sobressalente.


Coleções Java: resumo

A Tabela 8 resume os atributos das coleções:

Tabela 8. Resumo dos atributos das coleções

ColeçãoDesempenhoCapacidade padrãoTamanho vazioSobrecarga de 10KDimensionado com precisão?Algoritmo de expansão
HashSetO(1)16144360KNãox2
HashMapO(1)16128360KNãox2
HashtableO(1)11104360KNãox2+1
LinkedListO(n)148240KSim+1
ArrayListO(n)108840KNãox1.5
StringBufferO(1)167224Nãox2

O desempenho das coleções Hash é muito melhor do que qualquer um das Lists, mas com um custo por entrada muito maior. Devido ao desempenho de acesso, se você estiver criando coleções grandes (por exemplo, para implementar um cache), será melhor usar uma coleção com base em Hash, independentemente da sobrecarga adicional.

Para coleções menores nas quais o desempenho de acesso não é um problema, Lists se torna uma opção. O desempenho das coleções ArrayList e LinkedList é aproximadamente o mesmo, mas suas áreas de cobertura da memória são diferentes: o tamanho por entrada da ArrayList é muito menor do que LinkedList, mas não é precisamente dimensionada. Se uma ArrayList ou LinkedList é a implementação certa de List a ser usada, depende de quão previsível o tamanho da List pode ser. Se o tamanho for desconhecido, uma LinkedList poderá ser a opção certa, pois a coleção conterá menos espaço vazio. Se o tamanho for conhecido, uma ArrayList terá uma sobrecarga de memória muito menor.

Escolher o tipo de coleção correto permite que você selecione o equilíbrio correto entre o desempenho da coleção e área de cobertura da memória. Além disso, você pode minimizar a área de cobertura da memória redimensionando corretamente a coleção para maximizar a proporção de preenchimento e minimizar o espaço inutilizado.


Coleções em uso: PlantsByWebSphere e WebSphere Application Server Version 7

Na Tabela 8, a sobrecarga da criação de uma coleção baseada em Hash com 10.000 entradas tem 360K. Considerando que é comum os aplicativos Java complexos serem executados com heaps Java dimensionados em gigabytes, isso não parece uma sobrecarga grande — a menos, é claro, que muitas coleções estejam sendo usadas.

A Tabela 9 mostra o uso do objeto de coleção como parte dos 206 MB de uso do heap Java quando o aplicativo de amostra PlantsByWebSphere fornecido com o WebSphere® Application Server Version 7 é executado sob um teste de carga de cinco usuários:

Tabela 9. Uso da coleção por PlantsByWebSphere no WebSphere Application Server v7

Tipo de coleçãoNúmero de instânciasSobrecarga total da coleção (MB)
Hashtable262.23426,5
WeakHashMap19.56212,6
HashMap10.6002,3
ArrayList9.5300,3
HashSet1.5511,0
Vector1.2710,04
LinkedList1.1480,1
TreeMap2990,03
Total306.19542,9

É possível ver na Tabela 9 que mais de 300.000 coleções diferentes estão sendo usadas — e que as próprias coleções, sem contar os dados que ela contém, somam 42,9 MB (21 por cento) dos 206MB de uso do heap Java. Isso significa que as possíveis economias de memória estarão disponíveis se você mudar os tipos de coleção ou se certificar de que os tamanhos das coleções sejam mais precisos.


Procurando proporções de preenchimento menores com o Memory Analyzer

A ferramenta IBM Monitoring and Diagnostic Tools for Java - Memory Analyzer (Memory Analyzer), disponível como parte do IBM Support Assistant pode analisar o uso da memória das coleções Java (consulte Recursos). Seus recursos incluem análises de proporções de preenchimento e os tamanhos das coleções. É possível usar essa análise para identificar quaisquer coleções candidatas para otimização.

Os recursos de análise de coleção no Memory Analyzer estão localizados no menu Open Query Browser -> Java Collections, como mostra a Figura 14:

Figura 14. Análise da proporção de preenchimento de coleções Java no Memory Analyzer

A consulta Collection Fill Ratio selecionada na Figura 14 é a mais útil para identificar coleções muito maiores do que o necessário. É possível especificar diversas opções para essa consulta, incluindo:

  • objetos: os tipos de objeto (coleções) nos quais você está interessado
  • segmentos: as faixas de proporção de preenchimento para agrupamento dos objetos

A execução da consulta com a opção de objetos definida como "java.util.Hashtable" e a opção de segmentos definidas como "10" produz o resultado exibido na Figura 15:

Figura 15. Análise no Memory Analyzer da proporção de preenchimento de Hashtables

A Figura 15 mostra que das 262.234 instâncias de java.util.Hashtable, 127.016 (48,4 por cento) delas estão completamente vazias e que quase todas elas têm apenas um pequeno número de entradas.

Em seguida, é possível identificar essas coleções selecionando uma linha da tabela de resultados e clicando com o botão direito do mouse para selecionar list objects -> with incoming references para ver quais objetos detêm essas coleções ou list objects -> with outgoing references para ver o que há dentro dessas coleções. A Figura 16 mostra os resultados da análise das referências recebidas para as Hashtable vazias e a expansão de algumas das entradas:

Figura 16. Análise das referências recebidas para Hashtables vazias no Memory Analyzer

A Figura 16 mostra que algumas das Hashtables vazias são de propriedade do código javax.management.remote.rmi.NoCallStackClassLoader .

Ao analisar a visualização Atributos no painel esquerdo do Memory Analyzer, é possível ver detalhes específicos sobre a própria Hashtable , como mostra a Figura 17:

Figura 17. Inspeção da Hashtable vazia no Memory Analyzer

A Figura 17 mostra que Hashtable tem um tamanho de 11 (o tamanho padrão) e que está completamente vazia.

Para o código javax.management.remote.rmi.NoCallStackClassLoader , talvez seja possível otimizar o uso da coleção:

  • Alocando de forma lenta a Hashtable: se for comum para a Hashtable estar vazia, fará sentido para a Hashtable ser alocada somente quando houver dados para armazenar dentro dela.
  • Alocando a Hashtable para um tamanho preciso: como o tamanho padrão foi usado, é possível que um tamanho inicial mais preciso possa ser usado.

Se uma ou ambas as otimizações são aplicáveis depende de como o código é normalmente usado, e quais dados são normalmente armazenados dentro dele.

Coleções vazias no exemplo PlantsByWebSphere

A Tabela 10 mostra o resultado da análise de coleções no exemplo PlantsByWebSphere para identificar aquelas que estão vazias:

Tabela 10. Uso de coleção vazia pela PlantsByWebSphere no WebSphere Application Server v7

Tipo de coleçãoNúmero de instânciasInstâncias vazias% vazia
Hashtable262.234127.01648,4
WeakHashMap19.56219.46599,5
HashMap10.6007.59971,7
ArrayList9.5304.58848,1
HashSet1.55186655,8
Vector1.27162248,9
Total304.748160.15652,6

A Tabela 10 Mostra quem, em média, mais de 50 por cento das coleções estão vazias, implicando que é possível conseguir economias consideráveis de área de cobertura da memória por meio da otimização do uso da coleção. É possível aplicar isso a vários níveis do aplicativo: no código de exemplo PlantsByWebSphere, no WebSphere Application Server e nas próprias classes das coleções Java.

Entre o WebSphere Application Server versão 7 e 8, foi realizado um trabalho para aprimorar a eficiência da memória nas coleções Java e nas camadas de middleware. Por exemplo, uma grande porcentagem de sobrecarga das instâncias de java.util.WeahHashMap ocorre devido ao fato de que ela contém uma instância de java.lang.ref.ReferenceQueue para lidar com as referências fracas. A Figura 18 mostra o layout de memória de um WeakHashMap para um Java runtime de 32 bits:

Figura 18. Layout de memória de um WeakHashMap para um Java runtime de 32 bits

A Figura 18 mostra que o objeto ReferenceQueue é responsável por reter 560 bytes de dados, mesmo se a WeakHashMap estiver vazia e ReferenceQueue não for necessária. Para o caso de exemplo de PlantsByWebSphere com 19.465 WeakHashMaps vazias, os objetos ReferenceQueue adicionam outros 10,9 MB de dados que não são necessários. No WebSphere Application Server versão 8 e no release Java 7 dos tempos de execução Java da IBM, a WeakHashMap passou por algumas otimizações: ela contém uma ReferenceQueue, que, por sua vez, contém um array de objetos de Referência . Esse array foi alterado para ser alocado de forma lenta — ou seja, somente quando os objetos forem adicionados a ReferenceQueue.


Conclusão

Há uma quantidade grande e talvez surpreendente de coleções em qualquer aplicativo, e ainda mais para aplicativos complexos. O uso de uma grande quantidade de coleções normalmente fornece escopo para conseguir economias significativas de área de cobertura da memória por meio da seleção da coleção certa, do dimensionamento correto e, possivelmente, pela alocação de forma lenta. Essas decisões podem ser tomadas de forma mais adequada durante o design e o desenvolvimento, mas também é possível usar a ferramenta Memory Analyzer para analisar seus aplicativos existentes para uma possível otimização da área de cobertura da memória.

Recursos

Aprender

Obter produtos e tecnologias

  • IBM Monitoring and Diagnostic Tools for Java - Memory Analyzer: o Memory Analyzer oferece os recursos de diagnóstico da Eclipse Memory Analyzer Tool (MAT) para as Máquinas virtuais da IBM para Java.
  • IBM Extensions for Memory Analyzer: o IBM Extensions for Memory Analyzer oferece outros recursos para depuração de aplicativos genéricos Java e recursos para depuração de produtos específicos do software IBM.
  • Eclipse Memory Analyzer Tool (MAT): MAT ajuda a encontrar fugas de memória e identificar problemas de alto consumo da memória.
  • Avalie produtos IBM da maneira que for melhor para você: faça download da versão de teste de um produto, avalie um produto on-line, use-o em um ambiente de nuvem ou passe algumas horas na SOA Sandbox aprendendo a implementar Arquitetura Orientada a Serviços de modo eficiente.

Discutir

  • IBM on troubleshooting Java applications: leia este blog, escrito por Chris Bailey e seus colegas, para obter notícias e informações sobre as ferramentas IBM para solução de problemas de seus aplicativos Java.
  • Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java, Software livre
ArticleID=807292
ArticleTitle=Do Código Java ao Heap Java
publish-date=05142014