Intermediate 10 min · March 05, 2026

Space Complexity Analysis — Recursive OOM in Production

8000 recursive calls consumed 8MB stack, crashing a microservice via OOMKilled.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Space complexity measures memory used by an algorithm as input size grows
  • Auxiliary space excludes input; total space includes it — mix them up in an interview and you're done
  • Recursion adds O(depth) stack space — often missed in analysis
  • A single O(n²) algorithm can blow through 1GB RAM with n=50,000 inputs
  • Production rule: know your container memory limit before choosing an algorithm that grows with input
  • Object overhead in high-level languages can triple your memory footprint — profile, don't guess
Plain-English First

Imagine you're packing for a camping trip. You could bring a giant suitcase with everything pre-sorted into labelled boxes (fast to find things, but heavy), or you could bring one small bag and figure things out as you go (light, but slower). Space complexity is just asking: 'How much luggage does my algorithm need?' Some algorithms are genius hikers — they solve the problem with almost no extra bags. Others are hoarders who need a whole truck. Knowing the difference lets you choose the right algorithm before your program runs out of memory at the worst possible moment.

Every developer learns about time complexity fairly quickly — nobody wants slow code. But memory is just as finite a resource, and in the real world it bites you in ways that are far harder to debug. A server running out of RAM doesn't politely slow down; it crashes, kills your process, or starts thrashing swap space until everything grinds to a halt. Space complexity is the discipline that keeps that from happening by letting you reason about memory usage before you ship.

The core problem space complexity solves is that two algorithms can produce identical results in identical time, yet one quietly consumes gigabytes of RAM while the other uses a handful of bytes. Without a framework for measuring memory, you're flying blind — you'll only discover the problem under production load, with real users waiting. Space complexity gives you a mathematical vocabulary to compare algorithms at design time, not incident-review time.

By the end of this article you'll be able to calculate the space complexity of any algorithm you write, distinguish between auxiliary space and total space (a distinction that trips up even experienced engineers in interviews), spot the sneaky hidden memory costs that beginners miss, and make confident trade-off decisions between time and space in your own code.

And here's the kicker: in containerized environments with tight memory limits, the difference between an O(1) and an O(n) algorithm can be the difference between a stable service and an OOMKill at 3 AM. You don't get to tune memory limits after the crash — you need to anticipate them. That's what makes space complexity a survival skill, not just an interview topic.

What is Space Complexity Analysis?

Space Complexity Analysis is a core concept in DSA. Rather than starting with a dry definition, let's see it in action and understand why it exists.

Here's the thing: you've probably watched a friend write a recursive Fibonacci function and then wonder why it crashes on n=50. That crash is space complexity in action. The recursion stack ate up all available memory. Space complexity quantifies that. It's the measure of memory an algorithm consumes as input size grows, expressed as a function of input size n. Unlike time complexity, which every developer talks about, space complexity is the silent killer in production. A server doesn't slow down when it runs out of memory — it dies.

Consider two ways to compute the sum of an array of n integers: one creates a prefix sum array (O(n) auxiliary space) for O(1) range queries; the other sums on the fly (O(1) auxiliary space) but costs O(n) per query. The difference isn't academic — it decides whether your application fits in a 512MB container or needs 2GB. That's the practical payoff of understanding space complexity.

But there's more. Space complexity isn't just about Big O; it's about the actual bytes consumed. A simple integer array of 1 million elements takes ~4MB if you use primitives. Use an ArrayList<Integer> and that balloons to ~32MB due to object overhead and boxing. When you're running on a 256MB container, that difference decides whether your service survives a traffic spike. So don't just memorise formulas — know what they mean on your actual platform.

ForgeExample.javaJAVA
1
2
3
4
5
6
7
// TheCodeForge — Space Complexity Analysis example
// Always use meaningful names, not x or n
package io.thecodeforge.example;

public class ForgeExample {\n    public static void main(String[] args) {\n        String topic = \"Space Complexity Analysis\";\n        System.out.println(\"Learning: \" + topic + \" 🔥\");\n\n        // Demonstrate memory difference between primitive and boxed\n        int n = 1_000_000;\n        int[] primitives = new int[n]; // ~4MB\n        Integer[] boxed = new Integer[n]; // ~4MB for references, plus 16B per object = ~20MB\n        System.out.println(\"Primitive array uses ~4MB, boxed ~20MB+ on most JVMs\");\n    }\n}",
        "output": "Learning: Space Complexity Analysis 🔥\nPrimitive array uses ~4MB, boxed ~20MB+ on most JVMs"
      }

Auxiliary Space vs Total Space — The Interview Trap

Auxiliary space is the extra memory an algorithm needs beyond the input. Total space is auxiliary + input. In interviews, if you give total space when asked for auxiliary, you'll lose points. Here's the rule: when analyzing space, always clarify 'auxiliary or total?' before answering. If the problem says 'space complexity' without qualifiers, assume auxiliary — but state it explicitly. For example, an in-place sort uses O(1) auxiliary space. If you count the input array, it's O(n) total — that's misleading because the input is given, not allocated by the algorithm. Senior engineers always make this distinction.

Here's why this matters in production: when you're sizing memory requirements for a deployment, you care about total space — everything that lives in your address space. But when you're comparing two algorithms, you want to isolate the overhead you control: auxiliary space. That's why the distinction exists. It's not a pedantic interview trick; it's a practical tool.

Interview Hack
Always start your space answer with 'Auxiliary space is O(...) because...'. That single phrase signals senior-level clarity. Even if you're wrong on the constant, the framing saves you.
Production Insight
A candidate said 'O(n) space' for in-place QuickSort and lost the offer.
The input array already existed — the algorithm used O(log n) stack space.
Always qualify auxiliary vs total. It's a signal you know how real memory works.
Key Takeaway
Always specify auxiliary vs total.
Input storage is not your algorithm's memory.
Stack frames are auxiliary — never forget them.
Which space metric should you report?
IfInterview question: 'What is the space complexity?'
UseState auxiliary space. Add 'excluding the input' to be explicit.
IfProduction concern: memory limit includes everything
UseCalculate total space: input size + auxiliary. Input may be shared (reference) or copied (value). Know your language.
IfYou're copying the input for mutation
UseTreat as auxiliary space because you created a new structure. Report O(n) auxiliary.
IfUsing recursion that implicitly uses the call stack
UseInclude stack frames as auxiliary space. Depth = O(n) for unbalanced recursion.
IfThe problem says 'without extra space'
UseThey mean O(1) auxiliary. Recursion is not allowed. Use iterative in-place methods.

Recursive Algorithms and Call Stack Space

Every recursive call pushes a stack frame containing local variables, return address, and saved registers. This costs memory. For linear recursion (depth = n), auxiliary space is O(n). For divide-and-conquer (e.g., binary search), depth is O(log n). The killer is backtracking: you keep adding frames until you hit a dead end, then pop them slowly. In production, a deeply nested recursion can overflow the thread stack (default 1MB in Java). The fix: convert to iterative or use tail-call optimization if supported. Languages like Scala optimize tail recursion; Java doesn't. So if you're writing Java, avoid deep recursion for large inputs.

But even iterative solutions can hide stack costs. An explicit stack using a Deque still uses memory proportional to the number of elements stored — that's your auxiliary space. The difference is that you control it explicitly rather than letting the JVM manage the call stack. That's a win because you can monitor and bound the memory use.

StackDepthDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TheCodeForge - Demonstrating stack memory with recursion
package io.thecodeforge.space;

public class StackDepthDemo {
    static int depth = 0;

    static void recursiveMethod() {
        depth++;
        if (depth % 1000 == 0) {
            System.out.println("Depth: " + depth);
        }
        recursiveMethod(); // will StackOverflow
    }

    public static void main(String[] args) {
        try {
            recursiveMethod();
        } catch (StackOverflowError e) {
            System.out.println("Max depth reached: " + depth);
            // On 1MB stack, around 10,000
        }
    }
}
Output
Depth: 1000
Depth: 2000
...
Max depth reached: 10234
Real Stack Sizes
Java default thread stack is 1MB per thread. Each recursive call uses ~1KB for a typical method. That gives you about 1000 calls before overflow. With deeper recursion (e.g., 10,000 nodes in a tree), you'll crash. Increase stack with -Xss2m, but that only buys time. Iterative is safer.
Production Insight
A log parser using recursion on deeply nested JSON (depth 5000) crashed the JVM.
Switching to an explicit stack (ArrayDeque) reduced memory from O(depth) to O(width).
Rule: if the depth is unbounded, never use recursion.
Key Takeaway
Recursion is not free space.
Every call adds a stack frame — O(depth) auxiliary space.
When depth can be large, always prefer iteration.

Space-Time Trade-offs — When Speed Costs Memory

You can often trade space for time. Memoization caches results, turning exponential time into polynomial, but uses O(states) space. Precomputing prefix sums costs O(n) space for O(1) range queries. Hash tables give O(1) lookups but use O(n) extra memory. The trade-off is real: on a memory-constrained system (mobile, container), you might choose an O(n²) time algorithm that uses O(1) space over a faster one that uses O(n) space. The decision depends on your constraints. Profile before optimizing. Often the naive approach with minimal space is good enough for your input sizes.

Here's a common trap: engineers assume that faster is always better. But when a hashmap-based caching solution pushes your container over its memory limit, the GC starts thrashing and your response times spike. Suddenly your 'fast' algorithm is slower than the brute-force one because it's spending 50% of CPU time in GC. Always profile both time and space under realistic load.

Think of it as a budget: you have a finite amount of memory per container. Every time you allocate a hashmap, you're spending from that budget. Spend wisely.

TradeOffExample.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
// TheCodeForge - Space-time trade-off: prefix sum vs direct sum
import java.util.*;

public class TradeOffExample {
    // O(n) space, O(1) query
    static class PrefixSum {
        int[] prefix;
        PrefixSum(int[] arr) {
            prefix = new int[arr.length];
            prefix[0] = arr[0];
            for (int i = 1; i < arr.length; i++)
                prefix[i] = prefix[i-1] + arr[i];
        }
        int rangeSum(int l, int r) { return prefix[r] - (l == 0 ? 0 : prefix[l-1]); }
    }

    // O(1) space, O(n) query
    static int rangeSumDirect(int[] arr, int l, int r) {\n        int sum = 0;\n        for (int i = l; i <= r; i++) sum += arr[i];\n        return sum;\n    }

    public static void main(String[] args) {
        int[] data = {1, 2, 3, 4, 5};
        PrefixSum ps = new PrefixSum(data);
        System.out.println("Range sum [1,3] with prefix: " + ps.rangeSum(1,3));
        System.out.println("Range sum [1,3] direct: " + rangeSumDirect(data,1,3));
    }
}
Output
Range sum [1,3] with prefix: 9
Range sum [1,3] direct: 9
The Memory Budget Mental Model
  • O(1): Fixed overhead — like a reusable water bottle.
  • O(n): Linear growth — pack a snack for each unit of travel.
  • O(n²): Quadratic — you're buying a second suitcase for every new item.
  • O(log n): Slow growth — depth of recursion in balanced trees.
  • Hidden costs: JVM object headers (12-16 bytes per object), alignment padding, and collection resizing overhead.
Production Insight
A real-time trading system used a hashmap cache for every market data tick to reduce latency.
The hashmap grew to 10GB, causing GC pauses of seconds. They switched to an array indexed by symbol ID (O(1) space with fixed size).
Trade-off rule: if the space grows with data volume and you have real-time constraints, fixed-size arrays beat unbounded hashmaps.
Key Takeaway
Space and time are a trade-off.
Choose based on your bottleneck: memory limits or latency requirements.
Profiling beats guessing — measure both dimensions in production.

Analyzing Space Complexity in Practice — Step-by-Step

To analyze space complexity of an algorithm: 1. Identify all variables, data structures, and recursion stacks created by the algorithm. 2. For each, determine its size as a function of input size n. 3. Sum them, but drop constants and lower-order terms (Big O). 4. Specify auxiliary vs total. Common patterns: - Iterating without extra storage: O(1) auxiliary. - Copying input array: O(n) auxiliary. - Recursion depth equals input: O(n) auxiliary. - Hashmap with all input elements: O(n) auxiliary. - 2D DP table of size n×n: O(n²) auxiliary. Watch for hidden space: function arguments passed by value create copies; Strings in Java are immutable so concatenation creates new objects; autoboxing of primitives in collections adds object overhead.

Let's walk through a concrete example: finding all pairs that sum to a target. The two-pointer approach (after sorting) uses O(1) auxiliary space. The hashmap approach uses O(n) auxiliary. Both are O(n) time. Which one should you use? If you're on a 256MB container with 1 million integers, the hashmap costs ~32MB for the map (including object overhead) – that's fine. But if your input is 100 million integers, the hashmap alone would consume over 3GB. The two-pointer approach wins. This is why you need to think in terms of actual constraints, not just complexity class.

Another trap: function arguments passed by value. If you pass a large array to a recursive function by value in C++ or JavaScript spread, each call copies it — that's O(n) auxiliary per call, leading to O(n²) if depth is n. Use references or pointers.

The Memory Budget Mental Model
  • O(1): Fixed overhead — like a reusable water bottle.
  • O(n): Linear growth — pack a snack for each unit of travel.
  • O(n²): Quadratic — you're buying a second suitcase for every new item.
  • O(log n): Slow growth — depth of recursion in balanced trees.
  • Hidden costs: JVM object headers (12-16 bytes per object), alignment padding, and collection resizing overhead.
Production Insight
A MapReduce job ran out of memory because the reducer kept a HashMap of all unique keys.
Each key was a String object with overhead: ~40 bytes for the String + table entry.
With 10 million keys, that's 400MB just for keys, not counting values.
Always consider object overhead — your language's memory model matters.
Key Takeaway
Trace every allocation: variables, recursion, collections.
Object overhead can dominate.
Always measure your memory budget against real constraints.

Profiling Space Usage in Production: From Theory to Reality

Theory gets you the Big O. But production memory problems are often caused by things the theory ignores: object overhead, cache alignment, GC pressure, and language-specific quirks. The best way to catch these is to profile early, not after the OOM. Use heap dump analysis tools (Eclipse MAT, JProfiler) to see exactly where memory goes. Compare the theoretical space complexity to the actual heap footprint. When they diverge, you've found a real problem — usually from hidden copies, inflated objects, or unbounded data structures masked by asymptotic analysis.

For example, a HashSet of 100k Integer objects uses ~8MB in theory (100k * 4 bytes + overhead), but in Java it's closer to 16MB due to Integer object headers and HashMap entry nodes. That 2x blowup can push you over a container limit. So always measure against real constraints after you calculate Big O.

Don't stop at heap dumps. Use runtime metrics: container memory usage, GC frequency, and allocation rates. A constantly growing heap despite stable input might indicate a memory leak, not just an algorithm problem. That needs a different debugging approach — but the space complexity analysis tells you what the baseline should be.

profile-space.shBASH
1
2
3
4
5
6
7
# TheCodeForgeQuick heap profile for Java apps
# 1. Take a heap dump
jmap -dump:live,format=b,file=heap.hprof <pid>
# 2. Analyze with MAT or jhat
# Look for: char[], String, HashMap$Node, Integer
# 3. Check object sizes for a specific class
jmap -histo:live <pid> | grep 'com.mycompany'
Profile Don't Guess
Always run a heap profile before assuming your algorithm's theoretical space matches reality. You'll be surprised how often they differ.
Production Insight
A team relied on theory (O(n) space) but shipped O(n) object overhead with boxing.
30 million Integer objects instead of int[] consumed 480MB vs expected 120MB.
Rule: use primitive arrays when memory is tight, not collections.
Key Takeaway
Theory gives you the limit; profiling gives you the truth.
The gap is usually language overhead — boxed types, object headers.
Always measure before you trust your Big O for memory.
Which profiling approach matches your symptom?
IfMemory grows with each request but never shrinks (leak suspect)
UseTake heap dump and compare with baseline from a healthy state.
IfMemory spikes and then returns to normal (GC friendly but high allocation)
UseProfile allocation rate using async-profiler or JFR.
IfSteady memory but high GC pauses
UseCheck object size and number of live objects. Array of primitives may be better.
IfOOM occurs only on large input
UseCalculate theoretical space with input size and compare to heap limit. Likely algorithm-level.

Language-Specific Memory Overheads: When O(n) Becomes O(n × Overhead)

The same algorithm written in different languages can have drastically different memory footprints. In Java, an int[] of size n uses ~4n bytes. In Python, a list of n integers uses ~8n bytes for the list pointers plus ~28n bytes for each integer object (since everything is an object) — total ~36n bytes. That's 9× the Java primitive version. In C++, a std::vector<int> uses ~4n bytes, same as Java primitives. But std::vector is a dynamic array that may overallocate by 50% during growth, so peak memory can be 1.5×.

When you're writing a space-sensitive system, choose your data structures wisely: - Java: use int[], long[] instead of ArrayList<Integer>. - Python: use array('i') or numpy arrays instead of list. - C++: use std::array or raw arrays if size is known. - C#: use T[] or Span<T> instead of List<T> when performance matters.

Also be aware of alignment padding. In C++, a struct with a char and an int may take 8 bytes instead of 5 due to alignment. In Java, the JVM may reorder fields but object headers add overhead. These details matter when you're counting bytes in a memory-constrained environment.

MemoryOverheadComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TheCodeForge - Memory overhead comparison
package io.thecodeforge.space;

import java.util.ArrayList;

public class MemoryOverheadComparison {
    public static void main(String[] args) {
        int n = 1_000_000;
        
        // Primitive int array
        int[] primitive = new int[n];
        // Rough memory: 4 bytes per element + array header ~24 bytes = ~4MB
        
        // ArrayList<Integer>
        ArrayList<Integer> boxed = new ArrayList<>(n);
        for (int i = 0; i < n; i++) boxed.add(i);
        // Rough memory: array of references (8 bytes each) + Integer objects (16 bytes each) + ArrayList overhead = ~24MB
        // That's 6x more for the same data!
        
        System.out.println("Primitive array used ~4MB, ArrayList used ~24MB");
    }
}
Output
Primitive array used ~4MB, ArrayList used ~24MB
Hidden Multipliers
A simple change from int[] to ArrayList<Integer> can increase memory usage by 6x. When you're running on 256MB containers, that difference is the line between stable and OOM.
Production Insight
A financial application stored 5 million trade IDs in an ArrayList<String>.
Each String had overhead of 24 bytes (Java 8) + char array (2 bytes per char).
Total memory was 300MB instead of an expected 80MB with a custom ID encoding.
Rule: when storing large numbers of fixed-size records, encode them into primitives.
Key Takeaway
Language overhead multiplies theoretical space.
Aim for primitive arrays and value types where possible.
Know your platform's object model — it can 10x your memory.

C++ Implementation Examples for Space Analysis

C++ gives you precise control over memory, but that means you must be even more careful about space. Unlike Java, where object overhead is hidden, C++ lets you choose between stack and heap, pass by value or reference, and use primitive arrays or STL containers. Each choice has a direct impact on space complexity.

Consider the classic duplicate-finding problem: given an array of integers, find if any duplicate exists. A naive solution using a std::unordered_set will store up to n elements, giving O(n) auxiliary space. But if you can sort the array in place (assuming you can modify it), you can solve it with O(1) auxiliary space. In C++

space_examples.cppCPP
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
#include <iostream>
#include <vector>
#include <unordered_set>
#include <algorithm>
using namespace std;

// O(n) auxiliary space – uses hash set
bool hasDuplicateHashSet(const vector<int>& arr) {
    unordered_set<int> seen;
    for (int x : arr) {
        if (seen.find(x) != seen.end()) return true;
        seen.insert(x);
    }
    return false;
}

// O(1) auxiliary space – modifies input by sorting
bool hasDuplicateInPlace(vector<int>& arr) {
    sort(arr.begin(), arr.end());  // O(log n) auxiliary (recursion stack)
    for (size_t i = 1; i < arr.size(); ++i) {
        if (arr[i] == arr[i-1]) return true;
    }
    return false;
}

// O(n) auxiliary space – copies vector by value (bad!)
void badFunction(vector<int> v) {  // copies entire vector
    // process v
}

int main() {
    vector<int> data = {1, 2, 3, 4, 5, 1};
    cout << hasDuplicateHashSet(data) << endl;  // prints 1
    cout << hasDuplicateInPlace(data) << endl;  // prints 1
    return 0;
}
Output
1
1
Pass by Value Trap
In C++, passing large objects by value creates a full copy. Always use const references unless you intend to modify a local copy. This mistake can turn O(1) auxiliary into O(n) or worse.
Production Insight
A C++ server process that parsed JSON recursively passed std::string by value into each recursive call. Each string was ~100KB, depth ~500 → 50MB of hidden copies, causing OOM. Switching to const std::string& fixed it instantly. Rule: in C++, reference semantics are the default for performance.
Key Takeaway
In C++, your space complexity depends on whether you're copying or referencing data. Use const references to avoid unnecessary copies, and prefer in-place algorithms when memory is tight.

Space Complexity in Graph Algorithms: Common Patterns

Graph algorithms introduce unique space considerations beyond simple arrays and recursion. The adjacency list representation itself uses O(V + E) space for the graph structure. Beyond that, almost every graph traversal needs a visited array of size O(V). BFS additionally uses a queue that can hold up to O(V) nodes in the worst case. DFS iterative (using an explicit stack) also uses O(V) space. For Dijkstra's algorithm, the priority queue may hold up to O(E) entries if we don't support decrease-key — that can be a hidden memory sink.

Production graph processing on large graphs (millions of nodes) often hits memory limits not because the algorithm is asymptotically bad, but because each node/edge object carries language overhead. In Java, a simple node object (with an integer ID and a list of neighbours) can easily consume 40+ bytes per node. Multiply by 10 million nodes and you're at 400MB just for the graph structure, before any algorithm starts. When you're operating a social network recommendation engine, that graph may not even fit in the container's memory. That's when you reach for external memory algorithms or compressed graph representations.

When analyzing space for graph algorithms, don't just count the obvious data structures. Include the adjacency list or matrix, visited arrays, recursion stack (if recursive), and any distance/shortest path arrays. For example, Floyd-Warshall uses O(V²) space for the distance matrix — that's a killer for V > 10,000. In practice, if you need all-pairs shortest paths on a large graph, you either accept the memory cost or use single-source algorithms per query and trade time for space.

Production Insight
A graph analytics job on a 1M-node graph crashed because the adjacency list used ArrayList for each node's neighbours.
Each neighbour list object consumed ~40 bytes overhead, blowing the 1GB heap.
Rule: for dense graphs, use arrays of edges or compressed sparse row formats.
Key Takeaway
Graph algorithms often hide O(V+E) memory in the representation itself.
Account for all structures: adjacency, visited, queues, distances.
Language overhead on each node/edge can dominate — compress when possible.
● Production incidentPOST-MORTEMseverity: high

The Recursive Nightmare That Killed a Microservice

Symptom
Pod crashed with OOMKilled after processing a deep directory tree (~8000 nested folders). Logs showed sudden memory spike then exit code 137.
Assumption
The team assumed recursion was safe because the algorithm was O(n) in time and used no explicit data structures (no lists, no maps).
Root cause
Each recursive call consumed ~1KB of stack space. With 8000 calls, total stack memory hit ~8MB — Java default thread stack is 1MB. The JVM couldn't allocate more stack and the OS killed it. The algorithm's auxiliary space was O(depth) where depth = input size, not O(1) as assumed.
Fix
Converted the recursion to an iterative BFS using a Deque (linked list). Memory usage became O(width) which was bounded by ~100 nodes, not the depth. Also added a configurable recursion limit guard for safety.
Key lesson
  • Recursion doesn't mean O(1) space — every call pushes a stack frame.
  • Always consider worst-case input size for stack depth (nested directories, deeply nested JSON, long linked list).
  • In containerized environments (especially with small memory limits), even moderate recursion can OOM instantly.
  • When memory is tight, prefer iterative solutions or use tail recursion if your language optimizes it.
Production debug guideSymptom → Action mapping for space-related production issues4 entries
Symptom · 01
Process grows monotonically and crashes after processing large input
Fix
Use jmap -histo to see object counts. If you see millions of temporary objects, your algorithm likely creates copies or uses O(n) auxiliary space. Check if you can mutate input in place.
Symptom · 02
Recursive method causes stack overflow on deep input
Fix
Measure stack depth: add a counter in the recursion. If depth exceeds ~5000 on a 1MB stack, convert to iterative (use explicit stack or BFS). Consider increasing thread stack size with -Xss if recursion is unavoidable.
Symptom · 03
Memory usage spikes during sorting operations
Fix
Check sorting algorithm choice. QuickSort in-place uses O(log n) space; MergeSort uses O(n) auxiliary. Use Java's Arrays.sort (dual-pivot quicksort for primitives, TimSort for objects) — TimSort uses O(n). For huge datasets, consider heap sort or external sort.
Symptom · 04
Cached results consume too much memory over time (memoization)
Fix
Use a bounded cache (LinkedHashMap with access-order eviction, or Caffeine). Without it, memoization tables grow unbounded, turning O(1) space into O(n). For recursive DP, use tabulation with explicit array size limits.
★ Quick Space Complexity DebuggingThree commands to identify algorithm-level memory issues in production
Java app memory grows with each request processing large data
Immediate action
Take a heap dump and inspect object sizes by class
Commands
jmap -dump:live,format=b,file=dump.hprof <pid>
jhat dump.hprof (or Eclipse MAT) -> look for arrays of primitives, String[] or user-defined objects
Fix now
Check if you're creating new copies of input. For example, new ArrayList<>(input) doubles memory. Use subList or streams. If sorting, prefer in-place sort.
Recursive method crashes with StackOverflowError on production data+
Immediate action
Check stack depth by temporarily logging recursion count per call
Commands
Add `int depth = Thread.currentThread().getStackTrace().length;` at start of method (warning: slow, use sparingly)
Set JVM flag `-XX:+PrintStackTraceOnHeapAllocation`? Actually use `-Xss512k` to see if reducing stack size makes it crash sooner
Fix now
Rewrite recursion as iteration. If recursion must stay, increase stack size: -Xss2m or use ForkJoinPool with managed blocker.
Large HashMap used for caching grows beyond expected limit+
Immediate action
Count distinct keys: `map.size()` vs expected cardinality
Commands
jmap -histo:live <pid> | grep 'java.util.HashMap\|java.util.concurrent.ConcurrentHashMap'
Enable GC logging: `-Xlog:gc*` to see GC frequency and pauses
Fix now
Replace with bounded cache (Caffeine/Guava). Set maximum size and eviction policy. Alternatively, use WeakHashMap if keys are ephemeral.
🔥

That's Complexity Analysis. Mark it forged?

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

Previous
Time Complexity Analysis
3 / 6 · Complexity Analysis
Next
Amortized Analysis