The Java "Requested array size exceeds VM limit" error message

Marcio Endo
August 8, 2022

The other day I saw this tweet exchange between Lorin Hochstein, Stuart Marks and Peter Lawrey. I follow the last two of them on Twitter.

The tweet was about the "Requested array size exceeds VM limit" OutOfMemoryError message. I have written about it in this previous post. But I feel it was kind of lost in all of the noise about the particulars of writing a List implementation.

Therefore, in this blog post, I will discuss exclusively about the mentioned error message.

Let's begin.

Theoretical maximum array length

To create a new, single dimension, array instance you use a array creation expression like so:

var a = new Object[123];

The value inside the brackets, i.e. the length of the new array, must be convertible to an int type. Therefore, the following does not compile:

// does not compile!
long length = 123L;
var a = new Object[length];

As a long value cannot be converted, at least not without an explicit cast, to an int value.

Additionally, the value inside the brackets cannot be negative. For example, the following statement:

var a = new Object[-1];

Fails at runtime with a java.lang.NegativeArraySizeException:

Exception in thread "main" java.lang.NegativeArraySizeException: -1
	at post.InvalidArrayCreation.main(InvalidArrayCreation.java:11)

To sum up, the rules are:

Therefore, and in theory, the maximum array length would be given by the Integer.MAX_VALUE constant:

assertEquals(Integer.MAX_VALUE, 2_147_483_647);

Let's try to instantiate an array using the theoretical maximum value. The following program does just that:

public class VMLimit {
  public static void main(String[] args) {
    var arr = new Object[Integer.MAX_VALUE];

    System.out.format("Actual array length is %,d%n", arr.length);
  }
}

When executed the program fails with an OutOfMemoryError. The message is the one that is the subject of this post:

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at post.VMLimit.main(VMLimit.java:20)

Our theoretical maximum length is obviously wrong. It hits a VM limit of some sorts. So the question becomes:

Actual maximum array length

From the error message we know the limit is imposed by the JVM somehow. So, in order to find out the actual maximum allowed array length, let's explore the JDK source code.

The array length validation code

If we grep for the "Requested array size..." message we can find the following method in the hotspot/share/oops/klass.cpp file:

void Klass::check_array_allocation_length(int length, int max_length, TRAPS) {
  if (length > max_length) {
    if (!THREAD->in_retryable_allocation()) {
      report_java_out_of_memory("Requested array size exceeds VM limit");
      JvmtiExport::post_array_size_exhausted();
      THROW_OOP(Universe::out_of_memory_error_array_size());
    } else {
      THROW_OOP(Universe::out_of_memory_error_retry());
    }
  } else if (length < 0) {
    THROW_MSG(vmSymbols::java_lang_NegativeArraySizeException(), err_msg("%d", length));
  }
}

I don't know C++. But this section is quite readable for a Java programmer.

It is a code that validates the array length prior to the actual object allocation. Or, at least, it is safe to assume it is.

Let's start from the bottom. If the length is negative, a java.lang.NegativeArraySizeException is thrown. This matches the new Object[-1] example from the previous section.

If length is greater than max_length then "our message" is set to an OutOfMemoryError. Next, the error is thrown.

The maximum array length calculation code

If we grep for the name of the previous method, we will see that the max_length argument is given by the following in the hotspot/share/oops/arrayOop.hpp file:

// Return the maximum length of an array of BasicType.  The length can passed
// to typeArrayOop::object_size(scale, length, header_size) without causing an
// overflow. We also need to make sure that this will not overflow a size_t on
// 32 bit platforms when we convert it to a byte size.
static int32_t max_array_length(BasicType type) {
  assert(type >= 0 && type < T_CONFLICT, "wrong type");
  assert(type2aelembytes(type) != 0, "wrong type");

  const size_t max_element_words_per_size_t =
    align_down((SIZE_MAX/HeapWordSize - header_size(type)), MinObjAlignment);
  const size_t max_elements_per_size_t =
    HeapWordSize * max_element_words_per_size_t / type2aelembytes(type);
  if ((size_t)max_jint < max_elements_per_size_t) {
    // It should be ok to return max_jint here, but parts of the code
    // (CollectedHeap, Klass::oop_oop_iterate(), and more) uses an int for
    // passing around the size (in words) of an object. So, we need to avoid
    // overflowing an int when we add the header. See CRs 4718400 and 7110613.
    return align_down(max_jint - header_size(type), MinObjAlignment);
  }
  return (int32_t)max_elements_per_size_t;
}

Once again, I don't know C++. Nor do I know internals of the JVM like HeapWordSize or MinObjAlignment. So take the following assumptions with a grain of salt.

Using the code above, let's try to "manually compute" the maximum allowed length for a Object[] type.

Given the following:

Putting all of the values in SpeedCrunch and we have the following result:

max_elements_per_size_t = 2_305_843_009_213_693_949

The result is much bigger than max_jint.

max_jint is defined as 0x7FFFFFFF which is the same value of Integer.MAX_VALUE (source).

Therefore, the maximum array length is given by the expression max_jint - header_size(type). In Java code this could be translated to:

var max = Integer.MAX_VALUE - 2;

For completeness, I should mention that, since MinObjAlignment = 1, the align_down function will simply return the first argument (as far as I can tell). Here is the source code of align_down.

Validating our computed maximum array length

Given our computed maximum value we expect our "Requested array size..." message to occur in two cases only:

new Object[Integer.MAX_VALUE];
new Object[Integer.MAX_VALUE - 1];

Let's assume creating an array with a length of Integer.MAX_VALUE - 2 does not fail with the "VM limit" message. An array of this size will require more than 8G of available heap space. So if we run the program with a heap smaller than that we expect the array creation to fail; except it should fail with a "Java heap space" message instead.

So let's use this fact to our advantage and write the following program:

public class MaxValueTest {
  public static void main(String[] args) {
    oom(0, "Requested array size exceeds VM limit");
    oom(1, "Requested array size exceeds VM limit");
    oom(2, "Java heap space");
    oom(3, "Java heap space");

    System.out.println("Test was sucessful");
  }

  private static void oom(int value, String expected) {
    try {
      System.out.println("Will create value=" + value);
      
      @SuppressWarnings("unused")
      var a = new Object[Integer.MAX_VALUE - value];

      throw new AssertionError("Expected creation error for value=" + value);
    } catch (OutOfMemoryError e) {
      var m = e.getMessage();

      if (!m.equals(expected)) {
        throw new AssertionError("""
        Expected:
        %s
        But got:
        %s
        """.formatted(expected, m));
      }
    }
  }
}

It tries to create four arrays. It expects an OutOfMemoryError to occur for all of them. For the first two, it expects a "Requested array size..." message. For the last two, it expects a "Java heap space" message instead.

Running the program produces the following output:

Will create value=0
Will create value=1
Will create value=2
Will create value=3
Test was sucessful

Great! Our computation seems to be correct.

Actual maximum array length is not a fixed value

From the "The maximum array length calculation code" section it is possible to conclude that the actual maximum array length is not a fixed value. It depends on a few variables:

Let's look into them.

System architecture

The maximum array length calculation code uses values that are dependent on the system architecture. In particular:

Remember that, for my 64-bit system, we calculated max_elements_per_size_t to be:

max_elements_per_size_t = 2_305_843_009_213_693_949

On the other hand, if we were to compute max_elements_per_size_t for a hypothetical 32-bit system we would get:

max_elements_per_size_t = 1_073_741_820

Which is about half of Integer.MAX_VALUE (assuming the computation is right).

Therefore the actual maximum array length varies greatly between a 32-bit system and a 64-bit one.

JDK version

As far as I can tell, the actual maximum value is not defined by neither the language specification nor the JVM specification. In other words, we can say it is an "implementation detail".

If this assumption is correct, at some future JDK version can we expect those two values to be equal? I personally think so. This comment in the JDK source code also hints at this possibility. But please, don't take my word for it.

As a "fun fact", in the JDK6 release, it was possible to create an array with the "maximum theoretical value".

I wrote this small program:

public class MaxValueJDK6Test {
  public static void main(String[] args) {
    Object[] a = new Object[Integer.MAX_VALUE];

    System.out.println("Actual array length=" + a.length);

    a[0] = "first";
    a[Integer.MAX_VALUE - 1] = "last";

    System.out.println(a[0]);
    System.out.println(a[Integer.MAX_VALUE - 1]);

    System.out.println("Test was sucessful");
  }
}

And I ran it using JDK6:

$ /opt/jdk/oracle-6/bin/java -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)
$ /opt/jdk/oracle-6/bin/java -Xmx16G -cp target/classes/ post.MaxValueJDK6Test
Actual array length=2147483647
first
last
Test was successful

The change seems to have occurred during JDK7. It runs with JDK1.7.0:

$ /opt/jdk/jdk1.7.0/bin/java -version
java version "1.7.0"
Java(TM) SE Runtime Environment (build 1.7.0-b147)
Java HotSpot(TM) 64-Bit Server VM (build 21.0-b17, mixed mode)
$ /opt/jdk/jdk1.7.0/bin/java -Xmx16G -cp target/classes/ post.MaxValueJDK6Test
Actual array length=2147483647
first
last
Test was successful

But fails with JDK1.7.0_80:

$ /opt/jdk/jdk1.7.0_80/bin/java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
$ /opt/jdk/jdk1.7.0_80/bin/java -Xmx16G -cp target/classes/ post.MaxValueJDK6Test
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at post.MaxValueJDK6Test.main(MaxValueJDK6Test.java:20)

Or it could have been backported. I don't know.

But, as mentioned, consider this as a "fun fact".

JVM parameters?

The actual maximum array length possibly depends on the parameters you start your JVM with. I say possibly because I did not verify this one.

From a previous section we saw that MinObjAlignment is defined by:

MinObjAlignment = ObjectAlignmentInBytes/HeapWordSize

ObjectAlignmentInBytes is a configurable value you can set when starting your JVM:

$ java -XX:+PrintFlagsFinal | grep ObjectAlignment
     intx ObjectAlignmentInBytes                   = 8                              {product lp64_product} {default}

Therefore, depending on how we set this property, we might get a different maximum array length value. But, once again, I did not actually verify this one.

On the object alignment subject I recommend this article by Aleksey Shipilëv.

Conclusion

In this blog post, we discussed about the maximum length of a Java array.

We saw that, in current JVM implementations, the actual maximum length is slightly smaller than the theoretical value. In other words, the actual maximum length is smaller than:

When the specified length is greater than a VM-specific computed maximum value, allocation will fail with an OutOfMemoryError with the following message:

Requested array size exceeds VM limit

This error will be thrown regardless of how much heap is available.

We saw the JVM code that calculates the maximum allowed array length. Finally, since the value is computed, we discussed the factors that affects its calculation.

You can find the source code of all of the examples in this post in this GitHub repository.