A matriz de bytes de Java de 1 MB ou mais ocupa duas vezes a RAM

14

A execução do código abaixo no Windows 10 / OpenJDK 11.0.4_x64 produz como saída used: 197e expected usage: 200. Isso significa que matrizes de 200 bytes de um milhão de elementos ocupam aprox. 200MB de RAM. Tudo bem.

Quando altero a alocação da matriz de bytes no código de new byte[1000000]para new byte[1048576](isto é, para 1024 * 1024 elementos), ela produz como saída used: 417e expected usage: 200. Que diabos?

import java.io.IOException;
import java.util.ArrayList;

public class Mem {
    private static Runtime rt = Runtime.getRuntime();
    private static long free() { return rt.maxMemory() - rt.totalMemory() + rt.freeMemory(); }
    public static void main(String[] args) throws InterruptedException, IOException {
        int blocks = 200;
        long initiallyFree = free();
        System.out.println("initially free: " + initiallyFree / 1000000);
        ArrayList<byte[]> data = new ArrayList<>();
        for (int n = 0; n < blocks; n++) { data.add(new byte[1000000]); }
        System.gc();
        Thread.sleep(2000);
        long remainingFree = free();
        System.out.println("remaining free: " + remainingFree / 1000000);
        System.out.println("used: " + (initiallyFree - remainingFree) / 1000000);
        System.out.println("expected usage: " + blocks);
        System.in.read();
    }
}

Olhando um pouco mais fundo com o visualvm, vejo no primeiro caso tudo como esperado:

matrizes de bytes ocupam 200mb

No segundo caso, além das matrizes de bytes, vejo o mesmo número de matrizes int ocupando a mesma quantidade de RAM que as matrizes de bytes:

matrizes int ocupam 200mb adicionais

Essas matrizes int, a propósito, não mostram que elas são referenciadas, mas não posso coletá-las com lixo ... (As matrizes de bytes mostram muito bem onde são referenciadas.)

Alguma idéia do que está acontecendo aqui?

Georg
fonte
Tente alterar dados de ArrayList <byte []> para byte [blocos] [] e, em seu loop for: dados [i] = new byte [1000000] para eliminar dependências nos internos do ArrayList
jalynn2
Poderia ter algo a ver com a JVM internamente usando um int[]para emular uma grande byte[]para melhor localidade espacial?
Jacob G.
@JacobG. definitivamente parece algo interno, mas não parece haver nenhuma indicação no guia .
Kayaman # 22/19
Apenas duas observações: 1. Se você subtrair 16 de 1024 * 1024, parece que funciona como esperado. 2. O comportamento com um jdk8 parece ser diferente do que pode ser observado aqui.
segunda
@ segundo Sim, obviamente, o limite mágico é se o array ocupa 1 MB de RAM ou não. Suponho que se você subtrair apenas 1, a memória será preenchida para obter eficiência de tempo de execução e / ou a sobrecarga de gerenciamento do array contará com 1 MB ... Engraçado que o JDK8 se comporte de maneira diferente!
Georg

Respostas:

9

O que isso descreve é ​​o comportamento pronto para o uso do coletor de lixo G1, que normalmente padroniza 1MB de "regiões" e se tornou um padrão da JVM no Java 9. A execução com outros GCs ativados fornece números variados.

qualquer objeto com mais da metade de um tamanho de região é considerado "enorme" ... Para objetos que são apenas ligeiramente maiores que um múltiplo do tamanho da região de heap, esse espaço não utilizado pode fazer com que o heap fique fragmentado.

Eu corri java -Xmx300M -XX:+PrintGCDetailse mostra que a pilha está esgotada por regiões enormes:

[0.202s][info   ][gc,heap        ] GC(51) Old regions: 1->1
[0.202s][info   ][gc,heap        ] GC(51) Archive regions: 2->2
[0.202s][info   ][gc,heap        ] GC(51) Humongous regions: 296->296
[0.202s][info   ][gc             ] GC(51) Pause Full (G1 Humongous Allocation) 297M->297M(300M) 1.935ms
[0.202s][info   ][gc,cpu         ] GC(51) User=0.01s Sys=0.00s Real=0.00s
...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Queremos que nosso 1MiB byte[]tenha "menos da metade do tamanho da região G1", portanto, a adição -XX:G1HeapRegionSize=4Mfornece um aplicativo funcional:

[0.161s][info   ][gc,heap        ] GC(19) Humongous regions: 0->0
[0.161s][info   ][gc,metaspace   ] GC(19) Metaspace: 320K->320K(1056768K)
[0.161s][info   ][gc             ] GC(19) Pause Full (System.gc()) 274M->204M(300M) 9.702ms
remaining free: 100
used: 209
expected usage: 200

Visão geral detalhada do G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

Detalhes de esmagamento do G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552

drekbour
fonte
Tenho mesmos problemas com GC série e com longa série que leva 8MB (e foi muito bem com o tamanho 1024-1024-2) e mudando G1HeapRegionSize não fez nada no meu caso
GotoFinal
Não estou claro sobre isso. Você pode esclarecer a chamada de java usada e a saída do código acima com um longo []
drekbour
@ GotoFinal, não observo nenhum problema não explicado acima. Testei o código com o long[1024*1024]qual o uso esperado é de 1600M com G1, variando em -XX:G1HeapRegionSize[1M usado: 1887, 2M usado: 2097, 4M usado: 3358, 8M usado: 3358, 8M usado: 3358, 16M usado: 3363, 32M usado: 1682]. Com -XX:+UseConcMarkSweepGCusado: 1687. Com -XX:+UseZGCusado: 2105. Com -XX:+UseSerialGCusado: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 apenas codifica assim, sem alterar nenhuma opção de GC, ele será impresso, used: 417 expected usage: 400mas se eu remover -2, mudará para used: 470cerca de 50 MB e 50 * 2
comprimentos
11
Mesma coisa. A diferença é de ~ 50 MB e você tem 50 blocos "enormes". Aqui está o detalhe do GC: 1024 * 1024 -> [0.297s][info ][gc,heap ] GC(18) Humongous regions: 450->4501024 * 1024-2 -> [0.292s][info ][gc,heap ] GC(20) Humongous regions: 400->400Prova que os últimos dois longos forçam o G1 a alocar outra região de 1 MB apenas para armazenar 16 bytes.
drekbour