Array byte Java 1 MB atau lebih membutuhkan dua kali RAM

14

Menjalankan kode di bawah ini pada Windows 10 / OpenJDK 11.0.4_x64 menghasilkan sebagai keluaran used: 197dan expected usage: 200. Ini berarti bahwa array 200 byte dari satu juta elemen membutuhkan sekitar. RAM 200MB. Semuanya baik-baik saja.

Ketika saya mengubah alokasi array byte dalam kode dari new byte[1000000]ke new byte[1048576](yaitu, ke 1024 * 1024 elemen), itu menghasilkan output used: 417dan expected usage: 200. Apa apaan?

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();
    }
}

Terlihat sedikit lebih dalam dengan visualvm, saya melihat dalam kasus pertama semuanya seperti yang diharapkan:

array byte memakan waktu 200MB

Dalam kasus kedua, selain array byte, saya melihat jumlah array int yang sama dengan jumlah RAM yang sama dengan array byte:

array int mengambil tambahan 200mb

Array int ini, omong-omong, tidak menunjukkan bahwa mereka direferensikan, tapi saya tidak bisa mengumpulkan sampah ... (Array byte menunjukkan dengan baik di mana mereka direferensikan.)

Ada ide apa yang terjadi di sini?

Georg
sumber
Coba ubah data dari ArrayList <byte []> ke byte [blok] [], dan di loop for: data [i] = byte baru [1000000] untuk menghilangkan ketergantungan pada internal ArrayList
jalynn2
Mungkinkah ada hubungannya dengan JVM secara internal menggunakan a int[]untuk meniru besar byte[]untuk lokalitas spasial yang lebih baik?
Jacob G.
@ JacobG. ini jelas terlihat sesuatu yang internal, tetapi sepertinya tidak ada indikasi dalam panduan ini .
Kayaman
Hanya dua pengamatan: 1. Jika Anda mengurangi 16 dari 1024 * 1024 tampaknya berfungsi seperti yang diharapkan. 2. Perilaku dengan jdk8 tampaknya berbeda maka apa yang bisa diamati di sini.
kedua
@ kedua Ya, batas magisnya jelas adalah apakah array membutuhkan 1MB RAM atau tidak. Saya berasumsi bahwa jika Anda mengurangi hanya 1, maka memori empuk untuk efisiensi runtime dan / atau overhead manajemen untuk jumlah array ke 1MB ... Lucu bahwa JDK8 berperilaku berbeda!
Georg

Jawaban:

9

Apa yang dijelaskan ini adalah perilaku out-of-the-box dari pengumpul sampah G1 yang umumnya default ke 1MB "region" dan menjadi default JVM di Java 9. Menjalankan dengan GC lain yang diaktifkan memberikan angka yang bervariasi.

benda apa pun yang lebih dari setengah ukuran wilayah dianggap "humongous" ... Untuk objek yang hanya sedikit lebih besar dari kelipatan ukuran wilayah tumpukan, ruang yang tidak digunakan ini dapat menyebabkan tumpukan menjadi terfragmentasi.

Saya berlari java -Xmx300M -XX:+PrintGCDetailsdan itu menunjukkan tumpukan habis oleh daerah humongous:

[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

Kami ingin 1MiB kami byte[]menjadi "kurang dari setengah ukuran wilayah G1" sehingga menambahkan -XX:G1HeapRegionSize=4Mmemberikan aplikasi fungsional:

[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

Ikhtisar mendalam tentang G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

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

drekbour
sumber
Saya memiliki masalah yang sama dengan GC seri dan dengan array panjang yang membutuhkan 8MB (dan baik-baik saja dengan ukuran 1024-1024-2) dan mengubah G1HeapRegionSize tidak melakukan apa pun dalam kasus saya
GotoFinal
Saya tidak jelas tentang ini. Bisakah Anda mengklarifikasi permohonan java yang digunakan dan menampilkan kode di atas dengan panjang []
drekbour
@ GotoFinal, saya tidak melihat ada masalah yang tidak dijelaskan di atas. Saya menguji kode long[1024*1024]yang memberikan perkiraan penggunaan 1600M Dengan G1, bervariasi menurut -XX:G1HeapRegionSize[1M digunakan: 1887, 2M digunakan: 2097, 4M digunakan: 3358, 8M digunakan: 3358, 16M digunakan: 3363, 32M digunakan: 1682]. Dengan -XX:+UseConcMarkSweepGCbekas: 1687. Dengan -XX:+UseZGCbekas: 2105. Dengan -XX:+UseSerialGCbekas: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 hanya kode seperti itu, tanpa mengubah opsi GC itu akan mencetak used: 417 expected usage: 400tetapi jika saya akan menghapus -2itu akan berubah used: 470jadi sekitar 50MB hilang, dan 50 * 2 lama pasti jauh lebih sedikit dari 50MB
GotoFinal
1
Hal yang sama. Perbedaannya adalah ~ 50MB, dan Anda memiliki 50 blok "humongous". Berikut detail 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->400Ini membuktikan bahwa dua lama terakhir memaksa G1 untuk mengalokasikan wilayah 1MB lainnya hanya untuk menyimpan 16 byte.
drekbour