Olá pessoal!
Nossa experiência com as etapas do curso
Java Developer continua e, por incrível que pareça, até com bastante sucesso (mais ou menos): como se viu, alavancar o planejamento de alguns meses com a próxima transição para uma nova etapa a qualquer momento conveniente é muito mais conveniente do que se Aloque quase seis meses para um curso tão difícil. Portanto, suspeita-se que são precisamente os cursos complexos que em breve começaremos a transferir lentamente para esse sistema.
Mas sou eu sobre a nossa, sobre otusovsky, me desculpe. Como sempre, continuamos a estudar tópicos interessantes que, embora não sejam abordados em nosso programa, mas que são discutidos conosco, preparamos uma tradução do artigo mais interessante em nossa opinião sobre uma das perguntas que nossos professores fizeram.
Vamos lá!

Coleções no JDK são as implementações da biblioteca padrão de listas e mapas. Se você observar um instantâneo de um aplicativo Java grande e típico, verá milhares ou até milhões de instâncias de
java.util.ArrayList
,
java.util.HashMap
etc. As coleções são indispensáveis para armazenar e manipular dados. Mas você já pensou se todas as coleções do seu aplicativo fazem um uso otimizado da memória? Em outras palavras, se o aplicativo travar com o vergonhoso
OutOfMemoryError
ou causar longas pausas no coletor de lixo, você já verificou as coleções usadas quanto a vazamentos.
Em primeiro lugar, deve-se notar que as coleções internas do JDK não são algum tipo de mágica. Eles são escritos em Java. O código fonte deles vem com o JDK, para que você possa abri-lo no seu IDE. Seu código também pode ser facilmente encontrado na Internet. E, como se vê, a maioria das coleções não é muito elegante em termos de otimização da quantidade de memória consumida.
Considere, por exemplo, uma das coleções mais simples e populares - a classe
java.util.ArrayList
. Internamente, cada
ArrayList
opera com uma matriz de
Object[] elementData
. É aqui que os itens da lista são armazenados. Vamos ver como esse array é processado.
Quando você cria um
ArrayList
com o construtor padrão, ou seja, chama
new ArrayList()
,
elementData
aponta para uma matriz genérica de tamanho zero (
elementData
também pode ser definido como
null
, mas a matriz fornece alguns pequenos benefícios de implementação). Quando você adiciona o primeiro elemento à lista, uma matriz única e real de
elementData
e o objeto fornecido é inserido nela. Para evitar alterar o tamanho da matriz a cada vez, ao adicionar um novo elemento, ele é criado com um comprimento igual a 10 ("capacidade padrão"). Acontece que: se você não adicionar mais elementos a este
ArrayList
, 9 dos 10 slots no array
elementData
permanecerão vazios. E mesmo que você limpe a lista, o tamanho da matriz interna não será reduzido. A seguir, é apresentado um diagrama desse ciclo de vida:

Quanta memória é desperdiçada aqui? Em termos absolutos, é calculado como (o tamanho do ponteiro do objeto). Se você usar o JVM HotSpot (que acompanha o Oracle JDK), o tamanho do ponteiro dependerá do tamanho máximo do heap (para obter mais detalhes, consulte
https://blog.codecentric.de/en/2014/02/35gb-heap-less- 32gb-java-jvm-memory-estranhezas / ). Normalmente, se você especificar
-Xmx
menor que 32 gigabytes, o tamanho do ponteiro será 4 bytes; para montões grandes - 8 bytes. Assim, um
ArrayList
, inicializado pelo construtor padrão, com a adição de apenas um elemento, desperdiça 36 ou 72 bytes.
De fato, um
ArrayList
vazio também
ArrayList
desperdiçando memória porque não carrega nenhuma carga de trabalho, mas o tamanho do
ArrayList
si não é zero e maior do que você provavelmente pensa. Isso ocorre porque, por um lado, todo objeto gerenciado pela JVM do HotSpot possui um cabeçalho de 12 ou 16 bytes, que é usado pela JVM para fins internos. Além disso, a maioria dos objetos na coleção contém um campo de
size
, um ponteiro para uma matriz interna ou outro objeto de "mídia de carga de trabalho", um campo
modCount
para rastrear alterações no conteúdo etc. Assim, mesmo o menor objeto possível que representa uma coleção vazia provavelmente precisará de pelo menos 32 bytes de memória. Alguns, como
ConcurrentHashMap
, ocupam muito mais.
Considere outra coleção comum - a classe
java.util.HashMap
. Seu ciclo de vida é semelhante ao ciclo de vida
ArrayList
:

Como você pode ver, um
HashMap
contendo apenas um par de valores-chave gasta 15 células internas da matriz, o que corresponde a 60 ou 120 bytes. Esses números são pequenos, mas a extensão da perda de memória é importante para todas as coleções no seu aplicativo. E acontece que alguns aplicativos podem gastar bastante memória dessa maneira. Por exemplo, alguns dos componentes populares do Hadoop de código aberto que o autor analisou perdem cerca de 20% de sua pilha em alguns casos! Para produtos desenvolvidos por engenheiros menos experientes que não passam por análises regulares de desempenho, a perda de memória pode ser ainda maior. Existem casos suficientes em que, por exemplo, 90% dos nós em uma árvore enorme contêm apenas um ou dois descendentes (ou nada) e outras situações em que o heap está entupido com coleções de 0, 1 ou 2 elementos.
Se você encontrar coleções não utilizadas ou subutilizadas em seu aplicativo, como corrigi-las? Abaixo estão algumas receitas comuns. Aqui, supõe-se que nossa coleção problemática seja um
ArrayList
referenciado pelo campo de dados
Foo.list
.
Se a maioria das instâncias da lista nunca for usada, tente inicializá-la lentamente. Então, o código que parecia anteriormente ...
void addToList(Object x) { list.add(x); }
... deve ser refeito em algo como
void addToList(Object x) { getOrCreateList().add(x); } private list getOrCreateList() {
Lembre-se de que algumas vezes você precisará tomar medidas adicionais para lidar com a concorrência em potencial. Por exemplo, se você oferecer suporte ao
ConcurrentHashMap
, que pode ser atualizado por vários segmentos simultaneamente, o código que o inicializa não deve permitir que dois segmentos criem duas cópias desse mapa aleatoriamente:
private Map getOrCreateMap() { if (map == null) {
Se a maioria das instâncias da sua lista ou mapa contiver apenas alguns itens, tente inicializá-los com uma capacidade inicial mais adequada, por exemplo.
list = new ArrayList(4);
Se suas coleções estiverem vazias ou contiverem apenas um elemento (ou um par de valores-chave) na maioria dos casos, você poderá considerar uma forma extrema de otimização. Funciona apenas se a coleção for totalmente gerenciada na classe atual, ou seja, outro código não poderá acessá-la diretamente. A idéia é que você altere o tipo do seu campo de dados, por exemplo, de Lista para um Objeto mais geral, para que agora possa apontar para uma lista real ou diretamente para um único item da lista. Aqui está um breve esboço:
Obviamente, o código com essa otimização é menos claro e mais difícil de manter. Mas isso pode ser útil se você tiver certeza de que isso economizará muita memória ou se livrará de longas pausas no coletor de lixo.
Você provavelmente já se perguntou: como descubro quais coleções no meu aplicativo consomem memória e quanto?
Resumindo: é difícil descobrir sem as ferramentas certas. Tentar adivinhar a quantidade de memória usada ou gasta pelas estruturas de dados em um aplicativo grande e complexo quase nunca leva a nada. E, sem saber exatamente para onde vai a memória, você pode gastar muito tempo perseguindo os objetivos errados, enquanto o aplicativo continua teimosamente
OutOfMemoryError
com o
OutOfMemoryError
.
Portanto, você deve verificar vários aplicativos usando uma ferramenta especial. Por experiência, a maneira mais ideal de analisar a memória da JVM (medida como a quantidade de informações disponíveis em comparação com o efeito dessa ferramenta no desempenho do aplicativo) é obter um despejo de heap e visualizá-lo offline. Um despejo de heap é essencialmente um instantâneo completo do heap. Você pode obtê-lo a qualquer momento chamando o utilitário jmap ou pode configurar a JVM para despejar automaticamente se o aplicativo travar com
OutOfMemoryError
. Se você pesquisar no Google "Dump de heap da JVM", verá imediatamente um grande número de artigos que explicam em detalhes como obter um dump.
Um dump de heap é um arquivo binário do tamanho de um heap da JVM, portanto, ele só pode ser lido e analisado usando ferramentas especiais. Existem várias ferramentas, tanto de código aberto quanto comerciais. A ferramenta de código aberto mais popular é o Eclipse MAT; também há o VisualVM e algumas ferramentas menos poderosas e menos conhecidas. As ferramentas comerciais incluem criadores de perfil Java de uso geral: JProfiler e YourKit, bem como uma ferramenta projetada especificamente para análise de heap dump - JXRay (aviso: último desenvolvido pelo autor).
Diferentemente de outras ferramentas, o JXRay analisa imediatamente o despejo de heap em busca de um grande número de problemas comuns, como linhas repetidas e outros objetos, além de estruturas de dados insuficientemente eficientes. Problemas com as coleções descritas acima se enquadram nesta última categoria. A ferramenta gera um relatório com todas as informações coletadas no formato HTML. A vantagem dessa abordagem é que você pode visualizar os resultados da análise em qualquer lugar a qualquer momento e compartilhá-los facilmente com outras pessoas. Você também pode executar a ferramenta em qualquer máquina, incluindo máquinas grandes e poderosas, mas "sem cabeça" no data center.
O JXRay calcula a sobrecarga (quanta memória você economizará se se livrar de um problema específico) em bytes e como uma porcentagem da pilha usada. Combina coleções da mesma classe que têm o mesmo problema ...

... e agrupa as coleções problemáticas acessíveis a partir de alguma raiz do coletor de lixo através da mesma cadeia de links, como no exemplo abaixo

Saber quais cadeias de links e / ou campos de dados individuais (por exemplo,
INodeDirectory.children
acima) indicam coleções que gastam a maior parte de sua memória permite identificar com rapidez e precisão o código responsável pelo problema e, em seguida, fazer as alterações necessárias.
Portanto, coleções Java insuficientemente configuradas podem desperdiçar muita memória. Em muitas situações, esse problema é fácil de resolver, mas às vezes você pode precisar modificar seu código de maneiras não triviais para obter melhorias significativas. É muito difícil adivinhar quais coleções precisam ser otimizadas para ter o maior impacto. Para não perder tempo otimizando as partes incorretas do código, você precisa obter um dump de heap da JVM e analisá-lo usando a ferramenta apropriada.
O FIM
Como sempre, estamos interessados em suas opiniões e perguntas, que você pode deixar aqui ou deixar
uma aula aberta e perguntar aos
professores lá.