Senior 6 min · March 05, 2026

Array Data Structure — Why 33MB Allocations Crashed Prod

A 4K pixel buffer allocates 33MB instantly.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Array = contiguous memory block holding elements of same type. Address of element i = base_address + (i * element_size) — this is why access is O(1)
  • Key components: contiguous storage (memory locality), zero-based indexing (offset from start), fixed size (determined at creation)
  • Performance insight: CPU loads 64-byte cache lines — array iteration touches 16 ints per cache miss, unlike pointer-chasing linked structures
  • Production trap: Unbounded array allocation from user input — new int[userInput] can cause OutOfMemoryError and DoS the service
  • Biggest mistake: Off-by-one in loops (i <= length instead of i < length) — crashes production after processing millions of records
Plain-English First

Picture a row of numbered lockers in a school hallway. Each locker has a number on the door starting from zero, and each one holds exactly one item. An array is exactly that — a row of numbered slots in your computer's memory, where each slot holds one piece of data and you can jump directly to any slot by its number. That numbering-from-zero thing trips people up at first, but it'll make perfect sense in a moment.

Every app you've ever used — your music player, your Instagram feed, the leaderboard in your favourite game — is secretly managing lists of data behind the scenes. Someone's playlist is a list of songs. A leaderboard is a list of scores. A shopping cart is a list of items. The moment you need to store and work with a collection of related things in code, you need a data structure — and the array is the simplest, fastest, and most fundamental one that exists. Learning arrays isn't just a box to tick; it's learning the raw material that almost every other data structure is built on top of.

Before arrays existed as a concept, programmers had to create a separate variable for every single item they wanted to store. Want to track scores for ten players? That's ten separate variables. Want to loop over them? You can't — you'd have to write the same code ten times by hand. Arrays solve this by letting you group related items under a single name and access any one of them instantly using a position number. One name, many values, instant access.

By the end you'll know exactly what an array is and why it's stored contiguously in memory, how to declare, populate, and iterate over an array in Java, the rules and limits of arrays and why those rules exist, the most common mistakes that crash production (the off-by-one error), and how to answer array questions confidently in a technical interview.

What an Array Actually Is — Memory, Indexes, and the Zero Rule

Your computer's RAM is basically a very long street of tiny numbered houses — billions of them. Each house stores exactly one byte of data. When your program needs to store something, the operating system hands it a chunk of consecutive houses on that street. An array is simply a reserved block of those consecutive memory slots, all the same size, all sitting right next to each other.

Because the slots are contiguous (back-to-back), the computer can do something brilliant: if it knows where the first slot starts and how big each slot is, it can calculate the address of ANY slot instantly using simple arithmetic. Want slot number 7? Start address + (7 × slot size). Done. No searching, no scanning. This is called O(1) random access — constant time, regardless of how large the array is.

Now, why does indexing start at zero and not one? Because the index isn't really a 'position number' — it's an offset from the start. The first element is zero steps away from the start, so its index is 0. The second is one step away, so its index is 1. Once that clicks, zero-based indexing feels completely natural.

In Java, every array has a fixed size decided at the moment it's created. You're essentially reserving those memory slots in advance. You can't grow or shrink it later — that constraint is the price you pay for the blazing-fast access speed.

io/thecodeforge/ds/ArrayMemoryDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.ds;

public class ArrayMemoryDemo {
    public static void main(String[] args) {

        // Declare and create an array of 5 student scores.
        // Java reserves 5 consecutive int-sized slots in memory right now.
        // Each int in Java takes 4 bytes, so this reserves 20 bytes in a row.
        int[] studentScores = new int[5];

        // Assign values to each slot using the index (offset from start).
        // Index 0 = first slot (0 steps from the start)
        // Index 4 = last slot (4 steps from the start)
        studentScores[0] = 91;  // first student
        studentScores[1] = 78;  // second student
        studentScores[2] = 85;  // third student
        studentScores[3] = 62;  // fourth student
        studentScores[4] = 99;  // fifth student

        // Access any element instantly by its index — no looping needed.
        // The JVM computes: base_address + (3 * 4 bytes) = value at index 3
        System.out.println("Score at index 3: " + studentScores[3]);

        // .length gives us the number of slots (declared size), not the last index.
        // Last valid index is always length - 1.
        System.out.println("Total slots reserved: " + studentScores.length);
        System.out.println("Last valid index    : " + (studentScores.length - 1));

        // Shorthand: declare and fill in one line using an array literal.
        // Java counts the values and sets the size automatically (5 here).
        int[] dailySteps = {4200, 8100, 6750, 9300, 7050};
        System.out.println("\nSteps on day 1 (index 0): " + dailySteps[0]);
        System.out.println("Steps on day 5 (index 4): " + dailySteps[4]);
    }
}
The Golden Rule of Array Indexes:
  • Index 0: first element. 0 steps from the start address.
  • Index length-1: last element. The maximum valid index.
  • Index length: DOES NOT EXIST. Accessing it throws ArrayIndexOutOfBoundsException.
  • Index -1: DOES NOT EXIST. Negative indexes are invalid in Java (unlike Python).
  • Loop condition: always i < array.length. Never i <= array.length.
Production Insight
A batch processing job iterated over a customer array with for (int i = 0; i <= customers.length; i++). The <= instead of < caused an ArrayIndexOutOfBoundsException on the last iteration. With 10 customers in dev, the exception was caught by the test framework. In production with 2.4 million customers, the exception occurred on iteration 2,400,001 — after processing all 2.4 million records successfully. The job's error handler treated the exception as a fatal failure and rolled back all 2.4 million processed records. One character (<= vs <) caused a complete data processing rollback affecting 2.4 million customers.
Rule: Always use i < array.length. Write it the same way every time until it is muscle memory. The cost of getting it wrong in production is disproportionate to the simplicity of the fix.
Key Takeaway
The boundary between 'works' and 'crashes' in array code is a single character: < vs <=. Always use i < array.length. Write it the same way every time until it is muscle memory. The cost of getting it wrong in production is disproportionate to the simplicity of the fix.

Worked Example — Array Operations Step by Step

Let arr = [10, 20, 30, 40, 50] (0-indexed, length 5).

  1. Access arr[2] → read memory at base + 2*sizeof(int) → returns 30. O(1).
  2. Update arr[2] = 35 → write 35 at that address → arr = [10,20,35,40,50]. O(1).
  3. Insert 25 at index 2 → shift elements 2..4 one position right → arr = [10,20,25,35,40,50]. O(n) because we moved 3 elements.
  4. Delete arr[2] (value 25) → shift elements 3..5 one position left → arr = [10,20,35,40,50]. O(n).
  5. Linear search for 40 → check index 0,1,2,3 → found at index 3. O(n) worst case.
  6. Binary search for 40 in sorted arr → mid=2 (35), 40>35 → search right half → mid=3 (40) → found. O(log n).

Key insight: arrays pay O(1) for random access but O(n) for insertion/deletion in the middle because elements must be shifted to maintain contiguity.

io/thecodeforge/ds/ArrayOperationsDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package io.thecodeforge.ds;

import java.util.Arrays;

public class ArrayOperationsDemo {
    public static void main(String[] args) {

        int[] arr = {10, 20, 30, 40, 50};

        // 1. Access by index — O(1)
        System.out.println("Access arr[2]: " + arr[2]); // 30

        // 2. Update by index — O(1)
        arr[2] = 35;
        System.out.println("After update  : " + Arrays.toString(arr));

        // 3. Insert at index 2 — O(n) shift
        int[] expanded = new int[arr.length + 1];
        // Copy elements before insertion point
        System.arraycopy(arr, 0, expanded, 0, 2);
        // Place new value
        expanded[2] = 25;
        // Shift remaining elements right
        System.arraycopy(arr, 2, expanded, 3, arr.length - 2);
        System.out.println("After insert  : " + Arrays.toString(expanded));

        // 4. Delete at index 2 — O(n) shift
        int[] contracted = new int[expanded.length - 1];
        System.arraycopy(expanded, 0, contracted, 0, 2);
        System.arraycopy(expanded, 3, contracted, 2, expanded.length - 3);
        System.out.println("After delete  : " + Arrays.toString(contracted));

        // 5. Linear search — O(n)
        int target = 40;
        int foundIndex = -1;
        for (int i = 0; i < contracted.length; i++) {
            if (contracted[i] == target) { foundIndex = i; break; }
        }
        System.out.println("Linear search for " + target + ": index " + foundIndex);

        // 6. Binary search — O(log n) requires sorted array
        Arrays.sort(contracted);
        int bsResult = Arrays.binarySearch(contracted, 40);
        System.out.println("Binary search for 40: index " + bsResult);
    }
}
Why Insert/Delete Is O(n) But Access Is O(1)
  • O(1) access: address = base + index * element_size. Single CPU instruction.
  • O(n) insert: shift all elements right from insertion point. Worst case: insert at index 0.
  • O(n) delete: shift all elements left from deletion point. Worst case: delete at index 0.
  • O(1) append: only when spare capacity exists. Dynamic arrays amortize this to O(1).
  • Binary search: O(log n) but requires sorted array. Sorting is O(n log n) one-time cost.
Production Insight
A real-time analytics dashboard inserted incoming events into a sorted array at the correct position to maintain order. At 10,000 events per second, each insert was O(n) with n growing to millions. The dashboard became unresponsive within 30 minutes.
Switch to a TreeMap (O(log n) insert) for the sorted structure, and use a raw array only for the final display snapshot rebuilt every 5 seconds.
Rule: arrays are optimal for read-heavy, fixed-size workloads. For insert-heavy sorted workloads, use a tree or heap structure.
Key Takeaway
Arrays give O(1) access at the cost of O(n) insertion/deletion in the middle. This trade-off is fundamental — it cannot be eliminated, only shifted to a different data structure. Choose arrays when reads dominate. Choose linked or tree structures when inserts dominate.

Reading, Writing, and Looping — Putting Arrays to Work

Storing data in an array is only half the story. The real power shows up when you loop over the array to process every element — calculating a total, finding the maximum, printing a list, or transforming values one by one.

Java gives you two clean ways to loop over an array. The classic for loop gives you the index as well as the value, which is essential when you need to know WHERE in the array you are (for example, to print 'Student 3 scored 85'). The enhanced for-each loop is cleaner when you only care about the values themselves and don't need the index.

Knowing when to use each one is a small but important judgment call. If you need the index, use the classic for loop. If you just need to read every value, use for-each. Never use for-each when you need to write back to the array — it gives you a copy of each value, not direct access to the slot, so any changes you make won't stick.

We'll also look at two classic array problems every developer solves in their first week: finding the sum of all elements and finding the largest value. These two patterns — the accumulator and the running champion — come up constantly.

io/thecodeforge/ds/ArrayIterationPatterns.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.thecodeforge.ds;

public class ArrayIterationPatterns {
    public static void main(String[] args) {

        int[] monthlyRainfall = {45, 30, 55, 80, 120, 95, 110, 105, 70, 40, 25, 50};

        // ── PATTERN 1: Classic for loop (use when you need the index) ──────────
        System.out.println("=== Monthly Rainfall Report ===");
        String[] monthNames = {"Jan","Feb","Mar","Apr","May","Jun",
                               "Jul","Aug","Sep","Oct","Nov","Dec"};

        for (int i = 0; i < monthlyRainfall.length; i++) {
            System.out.println(monthNames[i] + ": " + monthlyRainfall[i] + " mm");
        }

        // ── PATTERN 2: Enhanced for-each (use when you only need values) ───────
        int totalRainfall = 0;
        for (int rainfall : monthlyRainfall) {
            totalRainfall += rainfall;
        }
        System.out.println("\nTotal annual rainfall: " + totalRainfall + " mm");
        System.out.printf("Monthly average      : %.1f mm%n",
                          (double) totalRainfall / monthlyRainfall.length);

        // ── PATTERN 3: Finding the maximum (running champion pattern) ───────────
        int peakRainfall = monthlyRainfall[0];
        int peakMonth    = 0;

        for (int i = 1; i < monthlyRainfall.length; i++) {
            if (monthlyRainfall[i] > peakRainfall) {
                peakRainfall = monthlyRainfall[i];
                peakMonth    = i;
            }
        }
        System.out.println("\nWettest month: " + monthNames[peakMonth]
                           + " with " + peakRainfall + " mm");
    }
}
Watch Out: For-Each Won't Save Changes to the Array
  • for-each variable: local copy of each element. Modifying it does NOT modify the array.
  • Classic for loop with arr[i]: direct access to the memory slot. Changes persist.
  • Rule: use for-each for reading. Use classic for loop for writing.
  • This bug is silent — no exception is thrown. The array simply stays unchanged.
  • IDEs sometimes warn about 'unused assignment' in for-each loops. Pay attention to that warning.
Production Insight
A data migration script used for-each to normalize phone numbers in a customer array. The script ran for 3 hours processing 5 million records. Every phone number was normalized inside the for-each loop — but the changes were discarded because the loop variable was a copy. The migration reported success. Three days later, customer support received complaints about invalid phone numbers.
The fix: switch to a classic indexed for loop. The 3-hour migration had to be re-run.
Rule: for-each for modification is a silent data corruption bug. Always verify that your changes persisted by spot-checking the array after the loop.
Key Takeaway
for-each gives you a copy, not a reference. Changes to the loop variable are silently discarded. Use for-each for reading. Use classic for loop for writing. This is not a style preference — it is a correctness requirement.

Arrays vs ArrayList — Knowing Which Tool to Reach For

Once you're comfortable with arrays, you'll quickly hit their biggest limitation: the size is fixed. You decide how many elements it holds when you create it, and that's it forever. In real applications — a chat app adding new messages, a shopping cart where items come and go — you often don't know the size in advance.

Java's ArrayList solves this. It's a resizable array under the hood (it literally uses an array internally, and silently creates a new bigger array when it gets full). You trade a tiny bit of performance for the flexibility of a dynamic size.

So when should you pick which one? Use a raw array when you know the exact size upfront and you're optimising for speed and memory — think storing the 12 months of the year, or the RGB values of every pixel in an image (millions of values where memory matters). Use an ArrayList when the size will change at runtime — a list of search results, a queue of orders, user-entered data.

For interview purposes, you'll mostly work with raw arrays because problems are designed to test your logic, not your knowledge of library classes. Understanding how arrays work is the foundation — ArrayList is just a convenient wrapper on top of that same concept.

io/thecodeforge/ds/ArrayVsArrayList.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.thecodeforge.ds;

import java.util.ArrayList;

public class ArrayVsArrayList {
    public static void main(String[] args) {

        // ── RAW ARRAY: Fixed size, fast, lightweight ─────────────────────────
        double[] weeklyTemperatures = {18.5, 21.0, 19.8, 23.4, 25.1, 22.7, 20.3};
        System.out.println("=== Fixed-Size Array (Weekly Temperatures) ===");
        for (int day = 0; day < weeklyTemperatures.length; day++) {
            System.out.printf("Day %d: %.1f°C%n", day + 1, weeklyTemperatures[day]);
        }

        // ── ARRAYLIST: Dynamic size, flexible ────────────────────────────────
        ArrayList<String> shoppingCart = new ArrayList<>();

        shoppingCart.add("Apples");
        shoppingCart.add("Bread");
        shoppingCart.add("Orange Juice");
        System.out.println("\n=== Dynamic ArrayList (Shopping Cart) ===");
        System.out.println("Cart after adding 3 items: " + shoppingCart);

        shoppingCart.remove("Bread");
        System.out.println("Cart after removing Bread: " + shoppingCart);
        System.out.println("Number of items in cart  : " + shoppingCart.size());

        System.out.println("First item in cart: " + shoppingCart.get(0));
    }
}
Pro Tip: .length vs .size()
  • array.length: property. No parentheses. Returns the declared size.
  • list.size(): method. With parentheses. Returns the current element count.
  • ArrayList<Integer> autoboxes int -> Integer. 4 bytes -> ~16 bytes per element.
  • For memory-critical code with millions of elements, use raw int[] not ArrayList<Integer>.
  • ArrayList doubles capacity when full. This causes a one-time O(n) copy. Amortized O(1) per add.
Production Insight
A team stored 10 million sensor readings per hour in an ArrayList<Integer>. Each Integer object was ~16 bytes (object header + int value + alignment padding). Total memory: 10M 16 = 160MB per hour. Switching to int[] with a pre-allocated size of 10M dropped it to 10M 4 = 40MB — a 4x reduction. Over 24 hours of buffering, this saved 2.88GB of heap.
The autoboxing overhead of ArrayList<Integer> is invisible at small scales but catastrophic at millions of elements.
Rule: Use primitive arrays for large datasets. Reserve ArrayList for cases where dynamic sizing is required and the element count is under 100,000.
Key Takeaway
ArrayList is a convenience wrapper around a raw array. Use it when size is dynamic. Use raw arrays when size is known and memory matters. ArrayList<Integer> autoboxes to ~16 bytes per element vs 4 bytes for raw int[]. At scale, this is a 4x memory difference and causes significant GC pressure.

Array Memory Layout in the JVM — Primitive vs Object Arrays

Understanding how the JVM stores arrays in memory is the key to understanding their performance characteristics. Every Java array is an object on the heap with a small header (typically 12-16 bytes depending on JVM compressed oops settings), followed by the element data. The .length field is stored in the array header, not as a separate variable.

Primitive arrays (int[], double[], char[]) store the actual values contiguously. Object arrays (String[], Object[]) store references (pointers) to objects elsewhere on the heap. This distinction has massive performance implications: iterating over an int[] touches one contiguous memory region, while iterating over a String[] touches N+1 memory regions (the reference array plus each String object).

An int[1000] allocates one contiguous block: 16 bytes header + 4000 bytes data = 4016 bytes. An Integer[1000] allocates 1001 objects: one array of 1000 references (16 + 8000 = 8016 bytes) plus 1000 Integer objects (~16 bytes each = 16000 bytes). Total: ~24016 bytes. Same logical data, 6x more memory.

io/thecodeforge/ds/ArrayMemoryLayout.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.ds;

public class ArrayMemoryLayout {
    public static void main(String[] args) {

        // Primitive array: values stored directly in the array memory
        int[] primitiveArray = new int[1_000_000];
        // Memory: 16 bytes header + 4 bytes * 1,000,000 = ~4MB contiguous block

        // Object array: stores REFERENCES to objects, not the objects themselves
        Integer[] objectArray = new Integer[1_000_000];
        for (int i = 0; i < objectArray.length; i++) {
            objectArray[i] = i; // autoboxing: creates 1M Integer objects on the heap
        }
        // Memory: 16 bytes header + 8 bytes * 1,000,000 references = ~8MB for references
        // PLUS: 1,000,000 * ~16 bytes per Integer object = ~16MB for objects
        // Total: ~24MB vs ~4MB for primitive array — 6x more memory

        System.out.println("Primitive array length: " + primitiveArray.length);
        System.out.println("Object array length   : " + objectArray.length);

        // Demonstrate that object arrays hold references, not values
        Integer a = 42;
        Integer[] refs = {a};
        a = 99; // reassign the variable
        System.out.println("\nArray still holds: " + refs[0]); // still 42 — the reference didn't change

        // Default values differ by type
        System.out.println("\nDefault values in new arrays:");
        System.out.println("int[] default    : " + new int[1][0]);       // 0
        System.out.println("boolean[] default: " + new boolean[1][0]);  // false
        System.out.println("double[] default : " + new double[1][0]);   // 0.0
        System.out.println("String[] default : " + new String[1][0]);   // null
    }
}
Primitive vs Object Arrays: The Memory Story
  • Primitive array: values stored inline. One contiguous memory block. Cache-friendly.
  • Object array: references stored inline, objects scattered on heap. Pointer-chasing. Cache-hostile.
  • int[1M]: ~4MB. Integer[1M]: ~24MB. 6x memory difference for the same logical data.
  • Default values: primitives are 0/false/0.0. Objects are null.
  • Array header: 12-16 bytes (JVM-dependent). Contains class pointer, length, and padding.
Production Insight
A financial trading application stored order book prices in an ArrayList<Double>. At 500,000 price updates per second, the autoboxing (double -> Double) created 500,000 objects per second, generating massive GC pressure. The GC spent 40% of CPU time collecting discarded Double objects.
Switching to a pre-allocated double[] with a circular buffer index eliminated all autoboxing. GC CPU dropped from 40% to 2%. Latency p99 dropped from 12ms to 0.8ms.
Rule: In hot paths, primitive arrays are not a micro-optimization — they are a macro-level performance requirement.
Key Takeaway
Primitive arrays store values inline in one contiguous block. Object arrays store references to scattered objects. At scale, the difference is 6x memory and 10-50x GC pressure. For hot paths and large datasets, always use primitive arrays.
● Production incidentPOST-MORTEMseverity: high

Image Processing Pipeline Crashes with OutOfMemoryError After Array Allocation Spike

Symptom
java.lang.OutOfMemoryError: Java heap space in production. Service restarted automatically but crashed again within 4 minutes. Grafana showed heap usage spiking to 100% on every request. GC logs showed full GC pauses of 2-4 seconds before the eventual OOM.
Assumption
A memory leak was accumulating objects without releasing them. The team spent 6 hours checking for unclosed streams, static collections, and listener registrations. They assumed the OOM was from a gradual leak, not from large transient allocations.
Root cause
The image resize service allocated a pixel buffer array sized to the input image dimensions: int[] pixels = new int[width height]. For 1080p images, this was 1920 1080 = 2,073,600 ints = ~8MB per request. For 4K images, this was 3840 2160 = 8,294,400 ints = ~33MB per request. The service had no input dimension validation and no maximum array size check. The JVM heap was configured at -Xmx2g. With 50 concurrent requests each allocating 33MB, the total was 1.65GB just for pixel arrays, plus framework overhead, leaving no headroom. The 'leak' was not a leak — it was massive transient allocations that GC could not reclaim fast enough between requests. Array allocation is eager and immediate: new int[N] allocates N 4 bytes the moment the line executes.
Fix
1. Added input validation: reject images larger than 2560x1440 (1440p) with a 400 Bad Request. This caps the pixel array at ~14MB per request. 2. Added a maximum array size constant: static final int MAX_PIXEL_BUFFER = 2560 1440. Any allocation request exceeding this throws IllegalArgumentException before the array is created. 3. Switched from per-request array allocation to a reusable buffer pool (ArrayDeque<int[]>). Pre-allocate 10 buffers at startup, check out one per request, return it after processing. This caps total array memory at 10 14MB = 140MB regardless of concurrency. 4. Increased heap to -Xmx4g as a safety margin. 5. Added a metric: image_resize_pixel_buffer_bytes histogram in Prometheus to track allocation sizes over time.
Key lesson
  • Array allocation is eager — new int[N] allocates N * element_size bytes immediately and blocks. If N comes from user input, validate it before allocating.
  • There is no such thing as a 'lazy array' in Java. The memory is committed the moment you call new. Unbounded user input into array sizes is a denial-of-service vector.
  • Buffer pooling (reusable arrays from a pool) caps total memory usage regardless of concurrency. This is how production image processing libraries (ImageIO, TwelveMonkeys) handle large pixel buffers.
  • OutOfMemoryError is not always a leak. Massive transient allocations that exceed GC throughput look identical. Check allocation rate, not just retained heap. jstat -gc shows allocation rate.
  • Always set a maximum array size for any allocation derived from external input. Make it a named constant, not a magic number.
Production debug guideSymptom-first investigation path for array problems in production Java applications.6 entries
Symptom · 01
ArrayIndexOutOfBoundsException in production logs
Fix
An index exceeds the array bounds. Check loop condition: uses <= instead of <, off-by-one in calculation, or the array was resized elsewhere but the index was computed from the old length. Stack trace shows the exact line — fix the boundary condition. Add assert index >= 0 && index < array.length for critical paths.
Symptom · 02
OutOfMemoryError: Java heap space with large array allocations visible in heap dump
Fix
A code path is allocating arrays larger than expected. Use jmap to capture a heap dump, open in Eclipse MAT, and sort by shallow heap to find the largest byte[] or int[] allocations. Check if the array size comes from user input or an unbounded data source (API parameter, file length, network buffer size).
Symptom · 03
Application latency spikes correlate with large array allocations visible in GC logs
Fix
Large array allocations (over 512KB in HotSpot) go straight to old generation, triggering full GC pauses. Check GC logs for 'Full GC' events. Reduce allocation rate by pooling buffers (reuse arrays) or streaming data instead of loading entire arrays into memory.
Symptom · 04
NullPointerException when accessing array elements
Fix
The array reference is null, or an element within an array-of-arrays (2D) is null. Check: was the array initialized? In a 2D jagged array, inner arrays may be null if only the outer array was created with new Object[rows][]. Also check that the array is not being cleared to null elsewhere in the code.
Symptom · 05
Incorrect results from array operations — values appear shifted or duplicated
Fix
Likely an off-by-one error in an insertion or deletion operation. Elements shifted in the wrong direction, or the loop boundary skipped the first or last element. Trace the array state before and after the operation with Arrays.toString(). The most common pattern: shift start index or loop direction wrong.
Symptom · 06
ConcurrentModificationException or race condition when multiple threads access the same array
Fix
Arrays are thread-safe for individual element reads/writes (atomic for primitives <= 32 bits), but NOT safe for compound operations like read-modify-write (arr[i]++ is three separate operations) or multi-element shifts. Use synchronized blocks, or switch to a CopyOnWriteArrayList for read-heavy workloads. Use AtomicIntegerArray for thread-safe primitive array operations.
★ Array Issue Triage CommandsRapid commands to isolate array-related production problems in JVM-based applications.
OutOfMemoryError with large array allocations
Immediate action
Capture heap dump and find largest arrays
Commands
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
jhat -port 7401 /tmp/heap.hprof
Fix now
Open heap dump in Eclipse MAT. Sort by shallow heap. Find the largest byte[] or int[] allocation. Check the allocation site in the dominator tree. Add input validation or buffer pooling.
ArrayIndexOutOfBoundsException in logs+
Immediate action
Find the exact line from the stack trace and check the loop boundary
Commands
grep -n 'ArrayIndexOutOfBounds' /var/log/app/error.log | tail -5
awk '/ArrayIndexOutOfBounds/,/^$/' /var/log/app/error.log
Fix now
Check if loop uses <= instead of <. Check if array length changed between calculation and access. Fix boundary condition and add length assertion before the loop.
Full GC pauses correlated with array-heavy processing+
Immediate action
Check GC logs for full GC frequency and allocation rate
Commands
jstat -gcutil <pid> 1000 10
grep -i 'full gc' /var/log/app/gc.log | tail -20
Fix now
If full GCs are frequent, large arrays are being allocated and promoted to old generation. Use jstat -gc to see allocation rate. Pool buffer arrays or stream data in chunks instead of loading entire arrays.
Incorrect array results — shifted or corrupted data+
Immediate action
Add debug logging to print array state before and after the operation
Commands
grep -n 'System.out.println(Arrays' src/**/*.java
echo 'Arrays.toString(array)' | grep -r --include='*.java'
Fix now
Insert Arrays.toString(array) before and after the insert/delete operation. Compare states to find where the shift went wrong. Common cause: loop starting at wrong index or shift direction reversed.
Suspected off-by-one causing wrong value at boundary+
Immediate action
Test the boundary conditions with smallest possible array (size 1, 2, 3)
Commands
grep -n 'for.*<=' src/**/*.java
grep -n 'for.*>=' src/**/*.java
Fix now
Replace <= with < for upper bounds. Replace >= with > for lower bounds when iterating upward. The pattern for (int i = 0; i <= arr.length; i++) is almost always wrong.
Array Data Structure Variants Compared
Feature / AspectRaw Array (int[])ArrayList<Integer>LinkedList
SizeFixed at creationDynamic — grows automaticallyDynamic — grows automatically
Access by indexO(1) directO(1) via method callO(n) must traverse nodes
Insert at endO(1) if space, impossible if fullO(1) amortizedO(1) with tail pointer
Insert at middleO(n) shiftO(n) shiftO(1) after reaching position
Delete at middleO(n) shiftO(n) shiftO(1) after reaching position
Memory per element (1M elements)~4MB (int)~24MB (Integer + references)~40MB (node + two pointers)
Cache localityExcellent — contiguousGood — internal array is contiguousPoor — nodes scattered on heap
Can store primitives?YesNo — autoboxes to wrapperNo — autoboxes to wrapper
Syntax to accessarr[2]list.get(2)list.get(2) but O(n) internally
Syntax to get sizearr.lengthlist.size()list.size()
Best use caseKnown size, performance-critical, large datasetsUnknown size, general purpose, under 100k elementsFrequent insert/delete at known positions
Supported utilitiesArrays.sort(), Arrays.copyOf()Collections.sort()Collections.sort()

Key takeaways

1
An array stores elements in contiguous (back-to-back) memory slots
this is WHY index-based access is O(1). The computer doesn't search; it does arithmetic to jump straight to the address.
2
Array indexes start at 0 because an index is an offset from the start address, not a position number. The first element is 0 steps away from the start.
3
A raw Java array has a fixed size set at creation time. If you need to add or remove elements dynamically, use ArrayList
which is just a smarter wrapper around an array under the hood.
4
For-each loops give you a copy of each element
use them for reading only. Use a classic indexed for loop whenever you need to write back to the array or need the index value.
5
Cache locality makes arrays 10-100x faster than linked structures for sequential access. Big-O is the same; real-world speed is not.
6
Primitive arrays (int[]) use 4 bytes per element. Object arrays (Integer[]) use ~16 bytes per element due to autoboxing. At scale, this is a 4x memory difference.
7
Always validate array sizes derived from external input. Unbounded array allocation is a denial-of-service vector.
8
Arrays.equals() compares contents. == compares reference identity. Always use Arrays.equals() for value comparison.

Common mistakes to avoid

7 patterns
×

Off-by-one error when looping — i <= array.length instead of i < array.length

Symptom
ArrayIndexOutOfBoundsException on the last iteration of the loop. Exception occurs at index length which doesn't exist. The program crashes.
Fix
Always use i < array.length. The last valid index is always length - 1. Use this pattern every time until it's muscle memory.
×

Confusing array declaration with array creation (null reference)

Symptom
NullPointerException at runtime when trying to access array[0]. The variable was declared (int[] scores;) but never initialized with new int[size].
Fix
Always combine declaration and creation: int[] scores = new int[5]; or use an array literal int[] scores = {90, 85, 78};. Both declare AND create in one step.
×

Trying to modify array elements inside a for-each loop

Symptom
Array remains unchanged after the loop finishes. No error message, no exception — just wrong results. The loop variable is a copy.
Fix
Use an indexed for loop for modifications: for (int i = 0; i < scores.length; i++) { scores[i] += 10; }. Verify changes with Arrays.toString(scores) after the loop.
×

Using ArrayList<Integer> for millions of elements when int[] would suffice

Symptom
OutOfMemoryError or excessive GC pauses. Each Integer autoboxing adds ~16 bytes vs 4 bytes for raw int. At 10 million elements, that is 160MB vs 40MB — a 4x memory difference.
Fix
Use primitive arrays (int[]) for large datasets. Reserve ArrayList for cases where dynamic sizing is required and the element count is under 100,000.
×

Using grid[0].length for all rows in a 2D jagged array

Symptom
ArrayIndexOutOfBoundsException on rows shorter than the first row. The inner loop assumes all rows have same length, but they don't.
Fix
Always use grid[row].length for each individual row in the inner loop. Better: validate uniform row lengths at load time if your data should be rectangular.
×

Allocating arrays from unbounded user input without validation

Symptom
OutOfMemoryError when a user submits a size of 1 billion. new int[1_000_000_000] allocates 4GB immediately, exhausting heap.
Fix
Always validate array size against a maximum constant before calling new. Reject or cap sizes that exceed the maximum. Add a metric to monitor array allocation sizes.
×

Assuming array equality compares contents (using == instead of Arrays.equals)

Symptom
arr1 == arr2 returns false even when both arrays contain the same values. In Java, == compares reference identity, not contents.
Fix
Use Arrays.equals(arr1, arr2) for 1D arrays or Arrays.deepEquals(arr1, arr2) for multi-dimensional arrays to compare element values.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the time complexity of accessing an element in an array by index...
Q02SENIOR
What's the difference between an array and an ArrayList in Java? When wo...
Q03SENIOR
Given an integer array, how would you find the second-largest element wi...
Q04SENIOR
Why does array insertion in the middle cost O(n) while access costs O(1)...
Q05SENIOR
What is the difference between a primitive array (int[]) and an object a...
Q06SENIOR
How would you implement a dynamic array (like ArrayList) from scratch us...
Q01 of 06JUNIOR

What is the time complexity of accessing an element in an array by index, and why? Can you explain the memory reason behind your answer?

ANSWER
Access is O(1) constant time. Arrays are stored in contiguous memory. The JVM computes the address as base_address + (index * element_size). This calculation is a single arithmetic operation (addition and multiplication) that takes the same amount of time regardless of the array size. There's no searching, no traversal, no guessing. This is the fundamental property of arrays: random access via index. The trade-off is that insertion and deletion are O(n) because elements must be shifted to maintain contiguity.
FAQ · 9 QUESTIONS

Frequently Asked Questions

01
Why do arrays start at index 0 instead of 1?
02
What happens if I try to access an index that doesn't exist in a Java array?
03
Can a Java array hold different types of data — like an int and a String together?
04
Why is array insertion O(n) when access is O(1)?
05
What is the difference between a static and dynamic array?
06
Why is array access O(1) even for a 1-billion-element array?
07
When should I use an array instead of a linked list?
08
How much memory does an array actually use in Java?
09
How do I compare two arrays for equality in Java?
🔥

That's Arrays & Strings. Mark it forged?

6 min read · try the examples if you haven't

1 / 13 · Arrays & Strings
Next
Two Pointer Technique