Running the code below on Windows 10 / OpenJDK 11.0.4_x64 produces as output used: 197
and expected usage: 200
. This means that 200 byte arrays of one million elements take up approx. 200MB RAM. Everything fine.
When I change the byte array allocation in the code from new byte[1000000]
to new byte[1048576]
(that is, to 1024*1024 elements), it produces as output used: 417
and expected usage: 200
. What the heck?
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();
}
}
Looking a bit deeper with visualvm, I see in the first case everything as expected:
In the second case, in addition to the byte arrays, I see the same number of int arrays taking up the same amount of RAM as the byte arrays:
These int arrays, by the way, do not show that they are referenced, but I can't garbage collect them... (The byte arrays show just fine where they are referenced.)
Any ideas what is happening here?
Yes, a byte variable in Java is in fact 4 bytes in memory.
If you try to access the array position (index) greater than its size, the program gets compiled successfully but, at the time of execution it generates an ArrayIndexOutOfBoundsException exception.
MAX_VALUE or about 2 billion regardless of the type of the array.
Max Size It generally depends on the JVM that we're using and the platform. Since the index of the array is int, the approximate index value can be 2^31 – 1. Based on this approximation, we can say that the array can theoretically hold 2,147,483,647 elements.
What this describes is the out-of-the-box behaviour of the G1 garbage collector which commonly defaults to 1MB "regions" and became a JVM default in Java 9. Running with other GCs enabled gives varying numbers.
any object that is more than half a region size is considered "humongous"... For objects that are just slightly larger than a multiple of the heap region size, this unused space can cause the heap to become fragmented.
I ran java -Xmx300M -XX:+PrintGCDetails
and it shows heap is exhausted by humongous regions:
[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
We want our 1MiB byte[]
to be "less than half the G1 region size" so adding -XX:G1HeapRegionSize=4M
gives a functional application:
[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
In depth overview of G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html
Crushing detail of G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With