Mảng byte Java từ 1 MB trở lên chiếm gấp đôi RAM


14

Chạy mã dưới đây trên Windows 10 / OpenJDK 11.0.4_x64 tạo ra như đầu ra used: 197expected usage: 200. Điều này có nghĩa là mảng 200 byte của một triệu phần tử chiếm khoảng. RAM 200MB. Mọi thứ đều ổn.

Khi tôi thay đổi phân bổ mảng byte trong mã từ new byte[1000000]thành new byte[1048576](nghĩa là thành 1024 * 1024 phần tử), nó tạo ra như là đầu ra used: 417expected usage: 200. Cái quái gì thế

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

Nhìn sâu hơn một chút với visualvm, tôi thấy trong trường hợp đầu tiên mọi thứ như mong đợi:

mảng byte chiếm 200mb

Trong trường hợp thứ hai, ngoài các mảng byte, tôi thấy cùng một số mảng int chiếm cùng một lượng RAM như các mảng byte:

mảng int chiếm thêm 200mb

Nhân tiện, các mảng int này không cho thấy rằng chúng được tham chiếu, nhưng tôi không thể thu thập chúng ... (Các mảng byte chỉ hiển thị tốt ở nơi chúng được tham chiếu.)

Bất kỳ ý tưởng những gì đang xảy ra ở đây?


Hãy thử thay đổi dữ liệu từ ArrayList <byte []> thành byte [blocks] [] và trong vòng lặp for của bạn: data [i] = new byte [1000000] để loại bỏ sự phụ thuộc vào phần bên trong của ArrayList
jalynn2

Nó có thể có một cái gì đó để làm với JVM trong nội bộ bằng cách sử dụng một int[]mô phỏng lớn byte[]cho địa phương không gian tốt hơn không?
Jacob G.

@JacobG. nó chắc chắn trông có vẻ gì đó bên trong, nhưng dường như không có bất kỳ dấu hiệu nào trong hướng dẫn .
Kayaman

Chỉ cần hai quan sát: 1. Nếu bạn trừ 16 từ 1024 * 1024 thì có vẻ như hoạt động như mong đợi. 2. Hành vi với jdk8 dường như khác nhau sau đó những gì có thể được quan sát ở đây.
thứ hai

@second Vâng, giới hạn kỳ diệu rõ ràng là liệu mảng có chiếm 1MB RAM hay không. Tôi giả sử rằng nếu bạn chỉ trừ 1, thì bộ nhớ được đệm cho hiệu quả thời gian chạy và / hoặc chi phí quản lý cho mảng được tính là 1MB ... Thật buồn cười khi JDK8 hành xử khác!
Georg

Câu trả lời:


9

Điều này mô tả là hành vi vượt trội của trình thu gom rác G1 thường mặc định là "vùng" 1MB và trở thành mặc định JVM trong Java 9. Chạy với các GC khác được kích hoạt sẽ cho các số khác nhau.

bất kỳ đối tượng nào có kích thước lớn hơn một nửa vùng được coi là "khiêm tốn" ... Đối với các đối tượng chỉ lớn hơn một chút so với nhiều kích thước vùng heap, không gian không sử dụng này có thể khiến cho vùng heap bị phân mảnh.

Tôi đã chạy java -Xmx300M -XX:+PrintGCDetailsvà nó cho thấy đống đã cạn kiệt bởi các khu vực hài hước:

[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

Chúng tôi muốn 1MiB của chúng tôi byte[]"nhỏ hơn một nửa kích thước vùng G1" vì vậy việc thêm vào -XX:G1HeapRegionSize=4Mmang lại cho ứng dụng chức năng:

[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

Tổng quan sâu về G1: https://www.oracle.com/technical-resource/articles/java/g1gc.html

Chi tiết nghiền của G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C55


Tôi có vấn đề tương tự với GC nối tiếp và với mảng dài mất 8 MB (và ổn với kích thước 1024-1024-2) và thay đổi G1HeapRegionSize không làm gì trong trường hợp của tôi
GotoFinal

Tôi không rõ về điều này. Bạn có thể làm rõ lời gọi java được sử dụng và đầu ra của đoạn mã trên với một []
drekbour dài

@GotoFinal, tôi không quan sát bất kỳ vấn đề nào không được giải thích ở trên. Tôi đã thử nghiệm mã với long[1024*1024]mức sử dụng dự kiến ​​là 1600M Với G1, thay đổi theo -XX:G1HeapRegionSize[1M được sử dụng: 1887, 2M được sử dụng: 2097, 4M được sử dụng: 3358, 8M được sử dụng: 3358, 16M được sử dụng: 3363, 32M được sử dụng: 1682]. Với -XX:+UseConcMarkSweepGCsử dụng: 1687. Với -XX:+UseZGCsử dụng: 2105. Với -XX:+UseSerialGCsử dụng: 1698
drekbour

gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 chỉ là mã như vậy, mà không thay đổi bất kỳ tùy chọn GC nó sẽ in used: 417 expected usage: 400nhưng nếu tôi sẽ loại bỏ -2nó sẽ thay đổi để used: 470nên khoảng 50MB đã mất hết, và 50 * 2 chờ đợi chắc chắn là ít hơn nhiều so với 50MB
GotoFinal

1
Điều tương tự. Sự khác biệt là ~ 50 MB và bạn có 50 khối "hài hước". Dưới đây là chi tiết về 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->400Điều đó chứng tỏ hai thời gian dài cuối cùng buộc G1 phải phân bổ vùng 1MB khác chỉ để lưu trữ 16 byte.
drekbour
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.