skip to content

What is the runtime cost of calling a varargs method, and when does it matter?

level: middleimportance: should knowfreq 45%

answer

  1. Each loose-arg call = one new heap array
  2. Cheap once, costly on hot paths (GC pressure)
  3. Fix: small fixed-arity overloads (List.of pattern)
  4. Passing an existing array = no allocation
  5. Escape analysis MAY remove it, but don't rely on it

basics

~10 s

Each varargs call (with loose arguments) creates a new array on the heap to hold them. That allocation is cheap individually but adds up on hot paths called millions of times, creating garbage-collection pressure.

solid answer

~50 s

Every time you call a varargs method by passing comma-separated values, the compiler allocates a fresh array on the heap to gather those arguments. For occasional calls this is negligible, but on a hot path executed millions of times per second it produces a stream of short-lived arrays that increase allocation and garbage-collection pressure, and can defeat some JIT optimizations. The classic mitigation, used throughout the JDK (e.g. EnumSet.of, List.of, String.format-adjacent APIs), is to provide a few fixed-arity overloads for the common small counts (1, 2, 3 args) and let varargs catch the rest — those overloads avoid the array entirely because fixed-arity beats varargs in resolution. You can also pass a reused/preallocated array directly to skip per-call allocation. Note that passing an existing array to a varargs parameter does not allocate, since the compiler just forwards the reference.

code

java · 10 lines
java
// Each loose-arg call allocates a new array:
for (int i = 0; i < 1_000_000; i++) {
    logger.info("value", i);   // new Object[]{"value", i} every iteration
}

// JDK-style mitigation: fixed-arity overloads beat varargs in resolution
static <E> Set<E> of(E e1)        { /* no array */ }
static <E> Set<E> of(E e1, E e2)  { /* no array */ }
static <E> Set<E> of(E... es)     { /* array, catch-all */ }
// of("a") and of("a","b") avoid allocation; of("a","b","c",...) uses the array

go deeper

for a junior

Knows that a varargs call creates an array behind the scenes, so there is some hidden work.

for a middle

Explains the per-call heap allocation, that it matters on hot paths via GC pressure, and that passing an existing array avoids it.

for a senior

Cites the small-fixed-arity-overload mitigation (List.of/EnumSet.of), ties it to overload resolution, and mentions escape analysis as an unreliable optimizer.

for a principal

Weighs API ergonomics vs allocation in latency-sensitive libraries, decides where to expose non-varargs fast paths, and reasons about JIT escape analysis limits when designing hot-path APIs.

## The hidden allocation Recall that `Type... name` is sugar: at each call site where you pass loose (comma-separated) arguments, **the compiler emits code to allocate a brand-new array** and fill it with those arguments before the call. So: ```java log("a", "b", "c"); // compiles to: log(new String[]{"a", "b", "c"}) ``` That `new String[]{...}` is a **heap allocation** that happens *every single time* the call executes. ## Why one allocation is fine but many are not A single small array allocation in Java is extremely cheap (a pointer bump in the young generation). The problem is **volume**. If a varargs method sits on a *hot path* — say, a logging or formatting call inside a tight loop running millions of times per second — you generate millions of short-lived arrays. These become **garbage** almost immediately, raising: - **Allocation rate** (CPU spent constructing arrays), and - **GC pressure** (more frequent young-generation collections). Even though escape analysis and scalar replacement *can* sometimes eliminate the array when the JIT proves it doesn't escape, you can't rely on that — it fails when the array is passed to a method that the JIT can't inline. ## The standard mitigation: small fixed-arity overloads The JDK's own answer, and the idiom to copy, is to **provide fixed-arity overloads for the common small argument counts and keep a varargs overload as the catch-all**: ```java static <E> List<E> of() { ... } // 0 args, no array static <E> List<E> of(E e1) { ... } // 1 arg, no array static <E> List<E> of(E e1, E e2) { ... } // 2 args, no array // ... up to of(E,...,E) for 10 ... static <E> List<E> of(E... es) { ... } // 11+ args, array ``` This is exactly what `List.of`, `Set.of`, `Map.of`, and `EnumSet.of` do. Because **fixed-arity overloads win overload resolution (phase 1) over the varargs overload (phase 3)**, calls with 0–10 elements bind to an allocation-free overload automatically; only larger calls pay for the array. Callers write the same code; the optimization is invisible. ## Other ways to avoid the cost - **Pass a reused array directly.** Since passing an existing array to a varargs parameter just forwards the reference (no new allocation), you can preallocate or reuse one buffer in a hot loop. - **Avoid varargs in the hottest inner loops** entirely, exposing a non-varargs API there. ## What does NOT cost extra - A **zero-argument** call still allocates an empty array — though the JDK often shares a single cached empty array, and `List.of()` and friends avoid it via the fixed overload. - **Passing an array directly** (`log(existingArray)`) does **not** allocate; the reference is passed through. ## Putting it in perspective For the vast majority of code — controllers, setup, occasional logging — the varargs allocation is irrelevant and clarity wins. The cost matters specifically in **high-frequency, low-level, latency-sensitive paths**, which is precisely where library authors add the small-arity overloads. Knowing the trade-off lets you keep varargs for readability while sidestepping it where the profiler says it hurts.

  • Why does List.of provide ten fixed-arity overloads plus a varargs one?
    To avoid the per-call array allocation for the common small element counts. Fixed-arity overloads win overload resolution, so 0–10 elements bind to an allocation-free method; only 11+ elements fall through to the varargs overload that builds an array.
  • Does passing an already-existing array to a varargs method allocate a new array?
    No. The compiler recognizes the argument is already the array type and passes the reference straight through. Allocation only happens when the compiler must gather loose comma-separated arguments.

saying these in an interview costs you the question

  • Claiming varargs allocates nothing because it's 'just sugar'
  • Saying the array is allocated once and reused across calls
  • Optimizing away varargs everywhere prematurely instead of only on hot paths
  • Believing passing an existing array still allocates a new one

context