Desenvolvimento

2 jun, 2016

Encontrando objetos duplicados com o Eclipse MAT

Publicidade

Eu escrevi anteriormente sobre como otimizar a memória no Eclipse, já olhando para a preponderância do new Boolean() (porque você nunca terá true ou false suficientes).

Recentemente, eu me perguntava quais seriam os estados de outros valores. Existem dois tipos interessantes: Strings e Integers. Strings, obviamente, ocupam muito espaço (então há mais efeito lá) mas e Integers? Bom, de volta a quando o Java começou, havia apenas o new Integer() se você quisesse obter um wrapper primitivo. No entanto, o Java 1.5 adicionou Integer.valueOf(), que é definido (pelo JavaDoc) para valores de cache na faixa de -128 …127 (isso foi porque autoboxing foi adicionado com generics, e autboxing usa Integer.valueOf() internamente).

Havia outros caches adicionados para outros tipos; o tipo Byte está totalmente em cache, por exemplo. Mesmo instâncias character são armazenadas em cache; nesse caso, o subconjunto de caracteres ASCII. Valores Long também são armazenados (embora possa ser que os valores normais sejam armazenados em um Long do lado de fora da faixa de cacheável, particularmente se forem timestamps).

Eu pensei que seria útil mostrar como usar o Eclipse MAT para identificar que tipos de problemas são esses e como corrigi-los. Isso pode ter benefícios tangíveis; no ano passado (graças à lembrança do Lars Vogel) eu fiz um commit com uma correção que foi o resultado da descoberta da sequência www.eclipse.org mais de 6.000 vezes na memória do Eclipse.

Depois de instalar o Eclipse MAT, não parece óbvio como usá-lo. O que ele oferece é um editor que pode entender a memória hprof dumps do Java, e gerar relatórios sobre eles. Então, a primeira coisa a fazer é gerar um heap dump em sequência de um processo Java para analisar.

Para este exemplo, eu baixei o Eclipse SDK versão 4.5 e, em seguida, importei um projeto plug-in existente de “Hello World”, que eu, então, executei como um aplicativo Eclipse e fechei. A principal razão para isso foi exercitar alguns dos caminhos envolvidos na execução do Eclipse e gerar mais do que apenas um heap mínimo.

Há muitas maneiras de gerar um heap; aqui, eu estou usando jcmd para executar um GC.heap_dump no sistema de arquivos local.

Usando jcmd para realizar um heap dump

$ jcmd -l
83845
83870 sun.tools.jcmd.JCmd -l
$ jcmd 83845 GC.heap_dump /tmp/45.hprof
83845:
Heap dump file created

Normalmente, a classe principal será mostrada pelo JCmd; mas para JVMs que são lançadas com um JRE incorporado ela pode ser vazia. Você pode ver o que há nela usando o comando VM.command_line:

Usando VM.command_line

$ jcmd 83845 VM.command_line
83845:
VM Arguments:
jvm_args: -Dosgi.requiredJavaVersion=1.7 -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -XX:MaxPermSize=256m -Xms256m -Xmx1024m -Xdock:icon=../Resources/Eclipse.icns -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -Dosgi.requiredJavaVersion=1.7 -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -XX:MaxPermSize=256m -Xms256m -Xmx1024m -Xdock:icon=../Resources/Eclipse.icns -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts
java_command: <unknown>
java_class_path (initial): /Applications/Eclipse_4-5.app/Contents/MacOS//../Eclipse/plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar
Launcher Type: generic

Abrindo o heap dump

Desde que o Eclipse MAT esteja instalado e o heap dump termine em .hprof, o dump pode ser aberto, indo para “Arquivo → Abrir arquivo…” e, em seguida, selecionando o heap dump. Depois de um breve assistente (você pode cancelá-lo), você terá a visão geral do heap dump:

mat-1

Isso mostra um gráfico tipo pizza que exibe quais classes contribuem para um maior espaço de heap; movendo o mouse sobre cada seção mostra o nome da classe. Na imagem acima, a fatia de 3,8MB do total é selecionada e mostra que é devido à classe org.eclipse.core.internal.registry.ExtensionRegistry.

Para descobrir onde existem objetos duplicados, podemos abrir o relatório Group By Value. Ele está sob o ícone azul à direita do perfil, no menu Java básico:

mat-2

Quando esse menu é selecionado, uma caixa de diálogo será apresentada, que permite que uma (ou mais) classe possa ser selecionada. Também é possível introduzir pesquisas mais específicas, tais como uma consulta OQL, mas isso não é necessário no começo.

Para descobrir quais são as strings duplicadas, entre com java.lang.String como tipo de classe:

mat-3

Isso, então mostra o resultado de todos os objetos que têm um método .toString(), incluindo o número de objetos e o shallow heap (a quantidade de memória ocupada pelos objetos diretos, mas não os dados referenciados):

mat-4

O tipo de resultado é ordenado pelo número de objetos e, em seguida, shallow heap. Nesse caso, há 2.338 instâncias de String que têm o valor true ocupando 56k, e 1.051 instâncias que têm o valor false. Você nunca pode estar muito certo sobre a verdade. (Eu quero saber o que é JEDgpPXhjM4QTCmiytQcTsw3bLOeXXziZSSx0CGKRPA= e por que precisamos de 300 deles…)

O impacto das strings duplicadas pode ser mitigado com o recurso -XX:+UseStringDeduplication do Java 8. Isso irá manter todas as 2.338 instâncias de String, mas vai rejuntar todos os elementos char para o mesmo array de caracteres de apoio. Não é um tune-up ruim, e para plataformas que exigem no mínimo Java 8, pode fazer sentido ativar isso como padrão. Claro que ferramentas (como o Eclipse MAT) não podem dizer quando isso está em uso ou não, assim você ainda pode ver os dados duplicados referenciados nos relatórios.

E sobre instâncias Integer? Bom, executando new Integer() é garantia para criar uma nova instância, enquanto Integer.valueOf() usa o cache integer. Vamos ver quantos integers realmente temos, executando o mesmo relatório Group By Value com java.lang.Integer como o tipo:

mat-5

Muito poucos, embora, obviamente, não são famintos por memória como as Strings são; na verdade, temos apenas 11k de instâncias Integer no heap. Isso mostra que pequenos números, tal como 0, 1, 2, e 8 são vistos sempre, como MAX_VALUE e MIN_VALUE. Corrigindo, nós conseguiríamos provavelmente obter em torno de 10k de volta no heap – uma quantia não enorme, com certeza.

O número 100 parece suspeito; nós temos alguns deles aparecendo também. Além disso, nenhum número de potência de dois parece sobressair. Então, como vamos encontrar de onde isso vem?

Uma característica do Eclipse MAT é ser capaz de entrar em um conjunto de objetos e, em seguida, mostrar as suas referências; quer recebidas (objetos que apontam para ele) ou enviadas (objetos para os quais ele aponta). Vamos ver de onde as referências vieram, clicando com o botão direito sobre 100 e, em seguida, em “List Objects → Incoming References”. Uma nova aba será aberta dentro do editor mostrando a lista de valores Integral, que podem ser expandida para ver qual a origem:

mat-6

Isso mostra 5 instâncias e o seu gráfico de referência. O último é o que foi criado pelo Integer embutido no cache, mas os outros todos parecem ter origem na classe org.eclipse.e4.ui.css.core.dom.properties.Gradiente, pela classe GradientBackground. Podemos abrir o código para ver uma List de objetos Integer, mas sem destinação:

Gradient.java

public class Gradient {
  private final List<Integer> percents = new ArrayList<>();
  public void addPercent(Integer percent) {
      percents.add(percent);
  }

Buscando referências na base de código para a chamada de método addPercent() nos leva à classe org.eclipse.e4.ui.css.swt.helpers.CSSSWTColorHelper:

CSSSWTColorHelper.java

public class CSSSWTColorHelper {
  public static Integer getPercent(CSSPrimitiveValue value) {
      int percent = 0;
      switch (value.getPrimitiveType()) {
      case CSSPrimitiveValue.CSS_PERCENTAGE:
          percent = (int) value
          .getFloatValue(CSSPrimitiveValue.CSS_PERCENTAGE);
      }
      return new Integer(percent);
  }
}

E aqui encontramos tanto a causa dos Integer duplicados e também o significado. Presumivelmente, há muitas referências nos arquivos CSS para 100% no gradiente, e cada vez que nos deparamos com ele instanciamos uma nova instância Integer, precisando ou não.

Ironicamente, se o método fosse apenas:

CSSSWTColorHelper.java

public class CSSSWTColorHelper {
        public static Integer getPercent(CSSPrimitiveValue value) {
                int percent = 0;
                switch (value.getPrimitiveType()) {
                case CSSPrimitiveValue.CSS_PERCENTAGE:
                        percent = (int) value
                        .getFloatValue(CSSPrimitiveValue.CSS_PERCENTAGE);
                }
                return percent;
        }
}

então, autoboxing teria começado, que usa Integer.valueOf() internamente, e que teria sido bom. Realmente, usar new Integer() é um código ruim e deve ser uma advertência; e sim, há um bug para isso.

E como é habitual, em muitos casos, Lars Vogel já arrumou o erro 489234:

Bug 489234

- return new Integer(percent);
 + return Integer.valueOf(percent);

Conclusão

Ser capaz de corrigir substituições para integers não é especificamente importante em si mesmo, mas percebendo que new Integer() (e new Boolean()) é um anti-pattern é o ponto de educacional aqui. De um modo geral, se você tem new Integer(x).intValue(), e substituir por Integer.parseInt(x) e em vez disso substituir por Integer.valueOf(x).

Na verdade, se você estiver retornando de um método que é declarado para ser do tipo Integer ou atribuindo a um campo de tipo Integer, então você pode apenas usar o valor literal, e ele será criado para o tipo certo com autoboxing (que usa Integer.valueOf() internamente). No entanto, se você está inserindo valores em um tipo de collection, então instanciar o objeto certo é uma ideia melhor.

Se você sabe que seu valor está fora do intervalo de cache, usar new Integer() terá exatamente o mesmo efeito que chamar Integer.valueOf(). Sob a otimização JIT para os métodos mais utilizados, seria de esperar que eles tenham o mesmo efeito. No entanto, note que Integer.valueOf() poderia mudar ao longo do tempo (por exemplo, para armazenar em cache MAX_VALUE), e você não seria capaz de tirar vantagem dele se utilizasse o construtor. Além disso, há também um interruptor de tempo de execução -Djava.lang.Integer.IntegerCache.high=1024 se você queria estender os valores em cache para mais integers. No entanto, isso só é atualmente respeitado para tipos Integer; outros wrappers primitivos não têm a mesma propriedade de configuração.

Além disso, ser capaz de olhar para objetos duplicados na memória e descobrir onde a memória heap está é uma ferramenta importante na compreensão de onde a memória do Eclipse é usada e o que pode ser feito para tentar resolver alguns desses problemas. Por exemplo, escavando uma referência de objeto resultou na descoberta de que P2 tem seu próprio cache Integer apesar de ter uma dependência mínima de Java 1.5 em que acrescentaram a Integer.valueOf(). Temos esperança de remediar isso.

Oh, e aquela string JEDgpPXhjM4QTCmiytQcTsw3bLOeXXziZSSx0CGKRPA=? Acontece que é um valor em META-INF/MANIFEST.MF para SHA-256 Digest de um monte de (presumivelmente vazios) arquivos de recursos no pacote com.ibm.icu:

com.ibm.icu.jar!META-INF/MANIFEST.MF

Name: com/ibm/icu/impl/data/icudt54b/cy_GB.res
SHA-256-Digest: JEDgpPXhjM4QTCmiytQcTsw3bLOeXXziZSSx0CGKRPA=

Name: com/ibm/icu/impl/data/icudt54b/ksb_TZ.res
SHA-256-Digest: JEDgpPXhjM4QTCmiytQcTsw3bLOeXXziZSSx0CGKRPA=

Na verdade, você pode não ser surpreendido ao saber que existem 300 deles 🙂

$ grep SHA-256-Digest MANIFEST.MF | sort | uniq -c | sort -nr
 300 SHA-256-Digest: JEDgpPXhjM4QTCmiytQcTsw3bLOeXXziZSSx0CGKRPA=
  65 SHA-256-Digest: Ku5LOaQNbYRE7OFCreIc9LWXXQBUHrrl1IhxJy4QRkA=
  61 SHA-256-Digest: TFNUA5jTkKhhjE/8DQXKUtrvohd99m5Q3LrEIz5Bj4I=
  53 SHA-256-Digest: p7PURP2WmyEtwG26wCbOYyN+8v3SjhinC5uUomd5uJA=
  53 SHA-256-Digest: fTZLTXXbc5Z45DJFKvOwo6f5yATqT8GsD709psc90lo=
  49 SHA-256-Digest: SiArmu+IqlRtLpSQb6d2F5/rIu6CU3lnBgyY5j2r7s0=
  49 SHA-256-Digest: A5xl6s5MaIPeiyNblw/SCEWgA0wRdjzo7e7tXf3Sscs=

Acontece que, para investigar uma otimização, você encontra uma outra potencial para otimização. O manifesto do organizador armazena o manifesto para os pacotes, que têm tanto a seção principal (onde as partes interessantes do manifesto estão), bem como todas as outras seções (incluindo suas assinaturas). Não tenho certeza de que isso é realmente necessário; isso foi introduzido no 865896, e o único lugar em que é usado é para tentar capturar um Specification-Title de diretório.

Como isso não é amplamente utilizado pelos OSGi, se modificar o runtime para não armazenar os dados de hash, podemos salvar ½Mb mais ou menos de strings redundantes, e as outras economias podem chegar a um par de megabytes ou mais:

mat-7

Sempre que houver uma otimização que pode ser aplicada, a discussão estará no erro 490008.

Update: graças ao Thomas Watson, a correção do bug foi discutida e integrada ao 4.5M7, que deve trazer uma melhoria de utilização de memória de um par de megabytes no runtime do Eclipse no futuro.

***

Alex Blewitt faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://alblue.bandlem.com/2016/03/duplicate-objects-mat.html