Reputation: 982
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?
Upvotes: 14
Views: 2585
Reputation: 3081
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
Upvotes: 9