Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect low available memory in Java

I'd like to detect when free memory is low (say 15% free) and take some action.
I tried using the Runtime memory indicators, but I seem to be getting inconsistent behavior.

Here's my test app:

import java.util.*;

public class Memory
{
    static final int BYTES_PER_MB = 1024*1024;
    
    public static void main(String[] args) {
        
        Queue<byte[]> q = new LinkedList<>();
        int allocated = 0;
        while (true) {
            q.add(new byte[BYTES_PER_MB * 100]);
            allocated += 100;
            System.out.printf("Allocated 100m (total allocated=%d)%n", allocated);
            printMem();
        }
    }
        
    public static void printMem() {
        long max = Runtime.getRuntime().maxMemory() / BYTES_PER_MB;
        long total = Runtime.getRuntime().totalMemory() / BYTES_PER_MB;
        long free = Runtime.getRuntime().freeMemory() / BYTES_PER_MB;
        long totalFree = free+(max-total); // allocated free + memory that can be allocated
        System.out.printf("MEM: max=%dm, total=%dm, free=%d, (total free=%d)%n", max, total, free, totalFree);
    }
}

Here are the results I'm getting:

JVM                                      | Command                         | Total free before OOM
--------------------------------------------------------------------------------------------------
Oracle Java 1.8.0_191-b12 64-Bit Server  | java -Xmx1024m -Xms10m Memory   | 309
Oracle Java 1.8.0_191-b12 64-Bit Server  | java -Xmx1024m Memory           | 205
Oracle Java 11.0.2+9-LTS  64-Bit Server  | java -Xmx1024m -Xms10m Memory   | 12
Oracle Java 11.0.2+9-LTS  64-Bit Server  | java -Xmx1024m Memory"          | 12

The results make sense for Java 11, but I don't understand the results for Java 8.

  1. Why am I unable to allocate memory, even though I have more than enough? (I don't think it's a fragmentation issue, since the heap has mostly 100m chunks + it also fails if I allocate in 10m chunks)
  2. Why can I allocate even less if I specify a low -Xmx? Shouldn't it only affect the initial heap size?
like image 949
Eli Finkel Avatar asked May 22 '26 04:05

Eli Finkel


1 Answers

  1. The default GC behavior between Java 8 and Java 11 is different. Tweaking GC parameters is one way to improve memory efficiency. In local tests, setting '-XX:+UseAdaptiveGCBoundary' in Java 8 caused OOM closer to heap memory limits.
  2. -Xmx controls the maximum amount of memory which is allowed to be allocated for the heap. The initial heap size can be controlled via: -Xms. If both values are the same, then the heap size will be fixed to the value set.

..

To better understand the memory breakdown between JVM versions it may be helpful to look at some visualizations. The below charts show the memory breakdown as memory is allocated by a similar program to the one included above in Java 8 and Java 15 (source included below):

Memory breakdown for Java 1.8

Memory breakdown for Java 15

Memory breakdown for Java 15 run using same GC as 1.8

Legend Description for Charts:
1) Total Free - Free memory including already allocated free memory and memory which may still be requested from the system up to Max memory. (totFre in test)
2) JVM Allocated (Free) - The memory which is available for usage within the memory already allocated for the JVM (Total Memory above). (freMem in test)
3) JVM Allocated - The memory currently allocated for the JVM. This may vary as objects are created or more space is needed up to Max Memory. (totMem in test)
4) Max Memory - The maximum memory the JVM will allow itself to use. Can be configured via -Xmx flag. (maxMem in test)

..

Why the discrepancy between Java 8 and 15?

When Java 15 is configured to use the same garbage collector used in Java 8 (Parallel GC), the heap grows similarly to how it does in Java 8. One difference is that in Java 15, all memory is consumed before failing with Out of Memory. This suggests that both the GC mechanism and parameters used impact the amount by which the heap is grown when more heap space is needed and the total effective heap capacity which can be used. This is further evidenced by running the same test in Java 8 with Serial GC in which case all memory is properly used. In the sample test, configuring the Parallel GC to use '-XX:+UseAdaptiveGCBoundary' improved max memory utilization before failure.

..

Test Source:

    public static void main(String[] args)
    {
        long totalProgramAllocated = 0;
        Queue<byte[]> dataQueue = new LinkedList<>();
        while (true)
        {
            System.out.println("Total Allocated: " + (totalProgramAllocated >> 20));
            byte[] nextAllocatedChunk = createArray(25);
            dataQueue.add(nextAllocatedChunk);
            totalProgramAllocated += nextAllocatedChunk.length;
            System.out.println(new MemoryUsage());
        }
    }

    private static final Random random = new Random();
    private static byte[] createArray(int megabytes){ byte[] data = new byte[(1 << 20) * megabytes]; random.nextBytes(data); return data; }
    
    public static final class MemoryUsage
    {
        public final long maxMem = Runtime.getRuntime().maxMemory();
        public final long totMem = Runtime.getRuntime().totalMemory();
        public final long freMem = Runtime.getRuntime().freeMemory();
        public final long totFre = maxMem - (totMem - freMem);
        @Override public String toString(){ return String.join(",", Long.toString(maxMem), Long.toString(totMem), Long.toString(freMem), Long.toString(totFre)); }
    }
like image 136
mike1234569 Avatar answered May 24 '26 23:05

mike1234569