> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Requested array size exceeds VM limit: What It Really Means
- URL: https://www.ggorantala.dev/requested-array-size-exceeds-vm-limit/
- Published: 2026-11-05T03:45:23.000Z
- Updated: 2026-11-05T03:45:23.000Z
- Description: Requested array size exceeds VM limit has nothing to do with free memory. Where Java's real max array length sits, why ArrayList stops early, and how to fix it.
- Author: Gopi Gorantala
- Tags: outofmemoryerror, java-errors

```java
byte[] page = new byte[Integer.MAX_VALUE];

```

That's the whole bug. Somebody on the thumbnailer team wanted "a buffer big enough for anything", reached for the biggest `int` Java has, and the service has been throwing `Requested array size exceeds VM limit` ever since. The fix looks obvious (use a smaller number), but the interesting part is that the number they should have used isn't `Integer.MAX_VALUE - 1` either, and it isn't the same number on every JVM configuration. I spent longer on that than I'd like to admit.

## The message, and its two look-alikes

```log
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at Lab.lambda$main$0(Lab.java:15)

```

That's OpenJDK 21.0.12.1, G1, `-Xmx64m`, Linux x86\_64\. Same text on Serial, Parallel and ZGC; I ran all four.

Two other messages get confused with it. On the same JDK, from the same kind of line of code, you can get:

```log
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Required array length 2147483639 + 9 is too large

```

Three messages, three different components, three different fixes. The first one in this article's title is the JVM saying "that length can't exist, so I won't even look at the heap". The second is the heap actually being full. The third is a JDK library class refusing to grow a buffer. If you read only "OutOfMemoryError" in your alerting you'll treat all three the same, and the first one is the only one where adding RAM is pointless.

## Which thing said no?

Free memory has no bearing on this one. Run it on a 64 GB box with `-Xmx48g` and it still fires. Two minutes to confirm:

- Look at the exact detail text. "Requested array size exceeds VM limit" comes from HotSpot, before any allocation attempt.
- Look at the stack. If the top frame is your own `new byte[n]` or `new Object[n]`, the number `n` is the suspect. If it's `Arrays.copyOf`, `ArrayList.grow`, `ByteArrayOutputStream.ensureCapacity`, something is growing a buffer toward the ceiling.
- Check GC logs. This error produces *no* GC. If you see a Full GC right before the error, it was `Java heap space`, not this.

Which thread got it isn't a puzzle here. Unlike a heap exhaustion, where some innocent thread takes the hit, this error lands on the thread that asked for the absurd length. That's the one honest stack trace in the whole OOM family.

## Sweep the boundary on your machine

You need nothing but a JDK. I used OpenJDK 21.0.12.1 (Ubuntu build), 2 cores, 8 GB, no container. Single file, run with `java Lab.java <mode>`:

```java
import java.io.ByteArrayOutputStream;

public class Lab {
    static void t(String name, Runnable r) {
        try { r.run(); System.out.println(name + " -> OK"); }
        catch (Throwable e) { System.out.println(name + " -> " + e); }
    }
    public static void main(String[] a) {
        int M = Integer.MAX_VALUE;
        switch (a[0]) {
            case "byte" -> {
                for (int d = 0; d <= 4; d++) {
                    final int n = M - d;
                    t("new byte[MAX-" + d + "]", () -> { byte[] x = new byte[n]; x[0] = 1; }); // the boundary sweep
                }
            }
            case "baos" -> t("ByteArrayOutputStream fill", () -> {
                ByteArrayOutputStream o = new ByteArrayOutputStream();
                byte[] chunk = new byte[16 << 20];
                long total = 0;
                try { while (true) { o.write(chunk, 0, chunk.length); total += chunk.length; } } // grows until the library says no
                catch (Throwable e) { System.out.println("stopped at " + total + " bytes: " + e); throw e; }
            });
        }
    }
}

```

Step 1, tiny heap, so the heap path is cheap to trigger:

```bash
java -Xmx64m Lab.java byte

```

```log
new byte[MAX-0] -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
new byte[MAX-1] -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
new byte[MAX-2] -> java.lang.OutOfMemoryError: Java heap space
new byte[MAX-3] -> java.lang.OutOfMemoryError: Java heap space
new byte[MAX-4] -> java.lang.OutOfMemoryError: Java heap space

```

There's the boundary: two lengths on this configuration are "impossible", everything below is merely "too big for this heap". I got the identical split for `long[]`, `boolean[]` and `Object[]`, and for `new ArrayList<>(Integer.MAX_VALUE)` (VM limit) versus `new ArrayList<>(Integer.MAX_VALUE - 8)` (heap space).

Step 2, give the heap enough room that the limit is the only thing in the way (needs about 3 GB free):

```bash
java -Xmx3g Lab.java byte

```

```log
new byte[MAX-0] -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
new byte[MAX-1] -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
new byte[MAX-2] -> OK
new byte[MAX-3] -> OK
new byte[MAX-4] -> OK

```

So `byte[Integer.MAX_VALUE - 2]` is a perfectly legal 2 GB array on this JVM. If you read a blog that says "the max is `Integer.MAX_VALUE - 8`" (you will, it's everywhere), that's a convention in JDK library code, not what HotSpot enforces. More on that in a minute.

Step 3, the library version of the same wall:

```bash
java -Xmx6g Lab.java baos

```

```log
stopped at 2130706432 bytes: java.lang.OutOfMemoryError: Required array length 2147483639 + 9 is too large

```

That took about 20 seconds, nearly all of it system time copying and zeroing 2 GB arrays. Teardown: nothing to delete; no dumps were written. If you add `-XX:+HeapDumpOnOutOfMemoryError` for the heap-space cases you'll leave a dump behind (see [the dump setup post](https://ggorantala.dev/heapdumponoutofmemoryerror/?ref=ggorantala.dev)), so clean it up.

Confirmation signal: the exact text above, in under a second, with no GC in the log.

## Short version

You asked for an array whose length is above what this JVM allows, so HotSpot threw before touching the heap. The limit is a little under `Integer.MAX_VALUE`, and where exactly depends on header size and object alignment. Free RAM, `-Xmx`, and the collector don't matter.

## Where the limit actually comes from

I assumed the number was a constant. It's a formula.

The check lives in HotSpot's array allocation path: `TypeArrayKlass::allocate_common` for primitive arrays and `ObjArrayKlass::allocate` for object arrays call a helper, `check_array_allocation_length`, against `arrayOopDesc::max_array_length(type)`. I read the first two in the `jdk-21+35` tag. I did not fetch the helper itself, so the exact line that raises the OutOfMemoryError I'm taking from the message text and the behaviour, not from reading it. What I did confirm is that the check runs before the heap allocator is called, which is why you never see a GC first.

`max_array_length` on 64-bit boils down to `align_down(max_jint - header_size_in_words, MinObjAlignment)`. The reason it subtracts the header: object sizes travel through the collectors as an `int` count of words, and header plus payload must not overflow that. (The source comment cites CRs 4718400 and 7110613 for that.)

That formula predicts what I measured, and it predicts what I hadn't measured yet:

| Config                                                | Header (words) | Alignment | Predicted max length | Measured              |
| ----------------------------------------------------- | -------------- | --------- | -------------------- | --------------------- |
| default (compressed class pointers, 8-byte alignment) | 2              | 1 word    | MAX-2                | MAX-2 OK, MAX-1 fails |
| \-XX:-UseCompressedClassPointers                      | 3              | 1 word    | MAX-3                | MAX-3 OK, MAX-2 fails |
| \-XX:ObjectAlignmentInBytes=16                        | 2              | 2 words   | MAX-3 (aligned down) | MAX-3 OK, MAX-2 fails |

All three rows came out as predicted on the first try, which is the closest thing to a victory lap this investigation had. The practical lesson: the boundary is configuration-dependent. A length that works in your lab can throw in a production JVM launched with different object-alignment or class-pointer flags. And the only value that is safe everywhere is "much less than that".

I couldn't test the newer compact object headers on this JDK (`-XX:+UseCompactObjectHeaders` is an unrecognized option on 21.0.12.1), so I won't say what the number is there. If the header shrinks I'd expect the boundary to move by a word, per the formula, but that's reasoning, not a run.

The dead end that cost me an afternoon: I assumed ZGC or Parallel might raise the ceiling because they handle huge objects differently. They don't. The check is above the collector. Four collectors, same two failing lengths.

## Why ArrayList and friends stop earlier

The JDK's own growable buffers don't try to reach the VM limit. `ArrayList`, `ByteArrayOutputStream` and `StringBuilder` all share one growth helper in `jdk.internal.util.ArraysSupport`, and in my run it stopped at length 2147483639, which is `Integer.MAX_VALUE - 8`. The eight-element margin is a safety convention for VMs whose header eats into the array. I didn't have `src.zip` on the lab box, so I'm inferring the constant from the numbers in the message (2147483639 + 9) rather than quoting source.

What it means for you:

- `ByteArrayOutputStream` and `StringBuilder` die at about 2 GB of content even when the heap could take more. In the lab the buffer reached 2,130,706,432 bytes (127 chunks of 16 MB) and the next 16 MB chunk produced `Required array length 2147483639 + 9 is too large`.
- This is a different message from this article's title, thrown by library code, not the JVM. The component answers the question "who said no": the JDK did, by choice.
- The stack shows `ArraysSupport.newLength`, with `ByteArrayOutputStream.ensureCapacity` or `AbstractStringBuilder.ensureCapacityInternal` underneath. A one-line fix will not help there. The buffer is the design problem.

## Fixing it, and what each fix costs

Cause before cure: you have one contiguous array and a requirement that doesn't fit in one. Pick by where the number came from.

- **A constant you chose** (`new byte[Integer.MAX_VALUE]`): replace it with the real size, or a chunk size. Costs nothing.
- **A length computed from untrusted input** (a header that says "payload is 3 GB"): validate against a cap *before* allocating, and reject with a 4xx. Costs a config value. If you don't, a hostile client picks the length for you.
- **An int overflow**: `width * height * 4` with a large image wraps negative, and you get a `NegativeArraySizeException` or a surprisingly small buffer, depending on luck. Use `Math.multiplyExact` or `long` math and fail loudly.
- **A genuinely huge payload**: stream it, or split into 64 MB chunks (`List<byte[]>`, or memory-mapped file segments). More code, flat memory.
- **Not a fix**: raising `-Xmx` or switching collectors. Neither moved the boundary in the lab (Serial, Parallel, G1 and ZGC all failed the same two lengths). Only object-header and alignment flags shift it, and by a single element.

If your array is genuinely between 1 GB and 2 GB, think twice anyway. With G1 that's a humongous allocation, needs contiguous regions, and is exactly the situation where `Java heap space` shows up on a heap that looks half empty. That belongs to [the G1 humongous objects post](https://ggorantala.dev/g1-humongous-objects/?ref=ggorantala.dev).

## The structure that never asks for 2 GB

Treat any size that comes from outside the process as a number to bound, not a number to trust. The thumbnailer ended up with a single method:

```java
static byte[] allocatePage(long declaredBytes) {
    final long CAP = 256L << 20;                       // 256 MB: product decision, not a JVM number
    if (declaredBytes < 0 || declaredBytes > CAP)
        throw new IllegalArgumentException("page too large: " + declaredBytes);
    return new byte[(int) declaredBytes];
}

```

Every allocation of an externally sized buffer goes through that, and the CAP lives in config. It's boring, which is the point.

## What to watch, and which JDKs I checked

Alert on the message text itself. Unlike `Java heap space`, this one is never "load"; any occurrence is a bug or an attack, so page on the first one. Pair it with a logged requested length if you control the allocation site.

A test that would have caught it: feed your parser a header claiming `Integer.MAX_VALUE` and assert a clean rejection, in a unit test, no big heap needed.

Version safety: everything above was run on OpenJDK 21.0.12.1 only. I did not run JDK 8, 11, 17 or 25\. The formula comes from `jdk-21+35` source; on other JDKs, re-run the sweep with `java -Xmx3g Lab.java byte` and read the boundary off the output rather than trusting mine. The `Required array length N + M is too large` wording is what I saw on 21; I haven't compared older wordings.

I'd not call this one a mystery after the fact, but it took more than a day of boundary sweeps and source reading to stop quoting the wrong constant. Hopefully you only need the table.

## Takeaways

- `Requested array size exceeds VM limit` is a length problem, thrown by HotSpot before any heap allocation or GC. Free memory is irrelevant.
- On 21.0.12.1 the largest legal `byte[]` was `Integer.MAX_VALUE - 2` by default, `- 3` with `-XX:-UseCompressedClassPointers` or 16-byte alignment. Don't hard-code it.
- `ArrayList`, `ByteArrayOutputStream` and `StringBuilder` stop at `Integer.MAX_VALUE - 8` with a different message. Different component, different message, same fix: stop building one giant array.
- Validate externally supplied sizes before allocating. Use `Math.multiplyExact` for size math.
- Raising `-Xmx` or switching collectors does nothing for this error.

## Questions people paste into search

**What does "Requested array size exceeds VM limit" mean in Java?**  
The code asked for an array longer than the JVM allows (a bit under `Integer.MAX_VALUE`). HotSpot rejects it before trying to allocate, so heap size and free memory don't matter.

**What is the maximum array size in Java?**  
On OpenJDK 21.0.12.1 with default flags I measured `Integer.MAX_VALUE - 2` elements for `byte[]`, `long[]`, `boolean[]` and `Object[]`. Different alignment or class-pointer flags lower it by one. Library collections stop earlier, at `Integer.MAX_VALUE - 8`.

**How is it different from "Java heap space"?**  
"Java heap space" means the length was legal but the heap couldn't hold it, so a GC ran first and failed to help. The VM-limit error never touches the heap. See [the OutOfMemoryError message map](https://ggorantala.dev/java-outofmemoryerror-types/?ref=ggorantala.dev).

**Will increasing -Xmx fix it?**  
No. In the lab, `-Xmx3g` changed which of `MAX-2` and below worked, but the two largest lengths failed identically at any heap size.