What are Memory Leaks?

Memory leaks occur when applications allocate memory but fail to release it, causing memory consumption to grow over time until the system runs out of resources. For QA engineers, detecting memory leaks early prevents production crashes and performance degradation. This is a critical aspect of performance testing.

Common Symptoms

  • Increasing memory usage over time
  • Application slowdown after extended use
  • Out of Memory (OOM) errors
  • Garbage collection pauses
  • System instability

JavaScript Memory Leaks

1. Detached DOM Nodes

// Memory leak - detached DOM nodes
let elements = [];

function createElements() {
    for (let i = 0; i < 1000; i++) {
        let div = document.createElement('div');
        div.innerHTML = `Element ${i}`;
        elements.push(div);  // Stored but never attached
    }
}

// Fixed - properly manage DOM references
function createElementsFixed() {
    const container = document.getElementById('container');

    for (let i = 0; i < 1000; i++) {
        let div = document.createElement('div');
        div.innerHTML = `Element ${i}`;
        container.appendChild(div);
    }

    // Clear references when done
    elements = [];
}

2. Event Listeners

// Memory leak - event listeners not removed
function setupButton() {
    const button = document.getElementById('myButton');

    button.addEventListener('click', function heavyHandler() {
        // Heavy operation with closure
        const largeData = new Array(1000000).fill('data');
        console.log(largeData.length);
    });
}

// Fixed - remove event listeners
function setupButtonFixed() {
    const button = document.getElementById('myButton');

    const handler = function() {
        const largeData = new Array(1000000).fill('data');
        console.log(largeData.length);
    };

    button.addEventListener('click', handler);

    // Cleanup
    return () => {
        button.removeEventListener('click', handler);
    };
}

// Usage
const cleanup = setupButtonFixed();
// Later: cleanup();

3. Timers and Intervals

// Memory leak - timer never cleared
class DataFetcher {
    constructor() {
        this.data = [];

        setInterval(() => {
            this.fetchData();
        }, 1000);
    }

    fetchData() {
        this.data.push(new Array(10000).fill('x'));
    }
}

// Fixed - clear intervals
class DataFetcherFixed {
    constructor() {
        this.data = [];
        this.intervalId = null;
    }

    start() {
        this.intervalId = setInterval(() => {
            this.fetchData();
        }, 1000);
    }

    stop() {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }

    fetchData() {
        this.data.push(new Array(10000).fill('x'));
    }
}

// Usage
const fetcher = new DataFetcherFixed();
fetcher.start();
// Later: fetcher.stop();

Browser DevTools Memory Profiling

Chrome DevTools Memory Profiler

// 1. Take heap snapshot
// Open DevTools → Memory tab → Heap snapshot → Take snapshot

// 2. Record allocation timeline
// Memory → Allocation instrumentation on timeline → Start

// 3. Identify leaks
// Compare snapshots:
// - Snapshot 1: Initial state
// - Perform actions
// - Snapshot 2: After actions
// - Compare to find retained objects

// Example test script
async function testMemoryLeak() {
    // Take baseline snapshot
    console.log('Taking baseline snapshot...');
    await takeHeapSnapshot('baseline');

    // Perform leaky operations
    for (let i = 0; i < 100; i++) {
        createElements();  // Leaky function
        await sleep(100);
    }

    // Take second snapshot
    console.log('Taking second snapshot...');
    await takeHeapSnapshot('after-leak');

    // Compare snapshots in DevTools
    console.log('Compare snapshots to identify leaks');
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

Analyzing Heap Snapshots

Key Metrics:

  • Shallow Size - Memory held by object itself
  • Retained Size - Memory freed if object is garbage collected
  • Retainers - Objects preventing garbage collection

Finding Leaks:

1. Sort by Retained Size (descending)
2. Look for unexpectedly large objects
3. Check Retainers path to understand why objects aren't released
4. Common culprits:
   - Event listeners
   - Closures
   - Global variables
   - Detached DOM nodes
   - Cache objects

Node.js Memory Leak Detection

Using heapdump

// Install: npm install heapdump
const heapdump = require('heapdump');
const express = require('express');
const app = express();

let leakyArray = [];

app.get('/leak', (req, res) => {
    // Intentional leak
    leakyArray.push(new Array(10000).fill('leak'));
    res.send(`Array size: ${leakyArray.length}`);
});

app.get('/snapshot', (req, res) => {
    const filename = `/tmp/heapdump-${Date.now()}.heapsnapshot`;
    heapdump.writeSnapshot(filename, (err) => {
        if (err) {
            res.status(500).send(err.message);
        } else {
            res.send(`Heap dump written to ${filename}`);
        }
    });
});

app.listen(3000);

// Test procedure:
// 1. curl http://localhost:3000/leak (multiple times)
// 2. curl http://localhost:3000/snapshot
// 3. Load .heapsnapshot in Chrome DevTools

Using clinic

# Install clinic
npm install -g clinic

# Run application with memory profiling
clinic doctor -- node app.js

# Generate load
ab -n 10000 -c 100 http://localhost:3000/

# Stop application (Ctrl+C)
# Clinic generates report showing memory growth

Python Memory Profiling

Using memory_profiler

# Install: pip install memory-profiler
from memory_profiler import profile

@profile
def memory_leak_example():
    """Function with memory leak"""
    leaked_list = []

    for i in range(1000):
        # Each iteration adds to list without cleanup
        leaked_list.append([0] * 10000)

    return len(leaked_list)

@profile
def memory_efficient():
    """Fixed version"""
    for i in range(1000):
        temp_list = [0] * 10000
        # Process temp_list
        del temp_list  # Explicit cleanup

    return "Done"

if __name__ == '__main__':
    print("Testing memory leak:")
    memory_leak_example()

    print("\nTesting efficient version:")
    memory_efficient()

# Run: python -m memory_profiler script.py

Using tracemalloc

import tracemalloc
import time

def find_memory_leak():
    """Detect memory leaks in Python"""
    tracemalloc.start()

    # Take snapshot 1
    snapshot1 = tracemalloc.take_snapshot()

    # Perform operations
    leaked_data = []
    for i in range(1000):
        leaked_data.append([0] * 10000)
        time.sleep(0.01)

    # Take snapshot 2
    snapshot2 = tracemalloc.take_snapshot()

    # Compare snapshots
    top_stats = snapshot2.compare_to(snapshot1, 'lineno')

    print("[ Top 10 memory allocations ]")
    for stat in top_stats[:10]:
        print(stat)

    tracemalloc.stop()

find_memory_leak()

Java/Kotlin Memory Leak Testing

Using JVisualVM

// Memory leak example
public class MemoryLeakExample {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            // Memory leak - objects never released
            leakyList.add(new byte[1024 * 1024]);  // 1MB
            Thread.sleep(100);

            System.out.println("Allocated: " + leakyList.size() + " MB");
        }
    }
}

// Run with monitoring:
// jvisualvm &
// Run application
// Monitor → Memory tab → Heap usage

Heap Dump Analysis

# Generate heap dump on OutOfMemoryError
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -jar application.jar

# Or manually trigger dump
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# Analyze with Eclipse MAT
# Download: https://www.eclipse.org/mat/
# Open .hprof file
# Run "Leak Suspects Report"

Mobile App Memory Testing

iOS - Instruments

// Memory leak example - Swift
class LeakyViewController: UIViewController {
    var timer: Timer?
    var data: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // Memory leak - strong reference cycle
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.data.append(String(repeating: "x", count: 10000))
        }
    }

    // Timer never invalidated - memory leak
}

// Fixed version
class FixedViewController: UIViewController {
    weak var timer: Timer?
    var data: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.data.append(String(repeating: "x", count: 10000))
        }
    }

    deinit {
        timer?.invalidate()
    }
}

// Testing with Instruments:
// 1. Xcode → Product → Profile → Leaks
// 2. Run app and perform actions
// 3. Check for leak indicators
// 4. Analyze retention cycles

Android - Memory Profiler

// Memory leak example - Android
class LeakyActivity : AppCompatActivity() {
    companion object {
        // Static reference causes leak
        private var staticContext: Context? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Memory leak - activity held in static variable
        staticContext = this
    }
}

// Fixed version
class FixedActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Use application context for static references
        val appContext = applicationContext
    }

    override fun onDestroy() {
        super.onDestroy()
        // Clean up resources
    }
}

// Testing with Android Profiler:
// 1. Run app in Android Studio
// 2. Open Profiler window
// 3. Click Memory → Record allocation
// 4. Perform actions (open/close activities)
// 5. Force GC and check for retained objects

Automated Memory Leak Detection

Automated testing complements manual profiling. For browser-based testing strategies, see our guide on API testing.

Puppeteer Memory Testing

// automated-memory-test.js
const puppeteer = require('puppeteer');

async function testMemoryLeaks() {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // Expose function to get memory usage
    await page.evaluateOnNewDocument(() => {
        window.getMemoryUsage = () => {
            return performance.memory ? {
                usedJSHeapSize: performance.memory.usedJSHeapSize,
                totalJSHeapSize: performance.memory.totalJSHeapSize,
                jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
            } : null;
        };
    });

    await page.goto('http://localhost:3000');

    const measurements = [];

    // Perform actions and measure memory
    for (let i = 0; i < 50; i++) {
        await page.click('#create-elements-button');
        await page.waitForTimeout(100);

        const memory = await page.evaluate(() => window.getMemoryUsage());
        measurements.push(memory);

        console.log(`Iteration ${i}: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
    }

    // Analyze results
    const memoryGrowth = measurements[measurements.length - 1].usedJSHeapSize -
                         measurements[0].usedJSHeapSize;

    const threshold = 50 * 1024 * 1024; // 50MB threshold

    if (memoryGrowth > threshold) {
        console.error(`❌ Memory leak detected: ${(memoryGrowth / 1024 / 1024).toFixed(2)} MB growth`);
        process.exit(1);
    } else {
        console.log(`✅ No significant memory leak detected`);
    }

    await browser.close();
}

testMemoryLeaks();

Prevention Best Practices

Code Review Checklist

## Memory Leak Prevention

### Event Listeners
- [ ] All event listeners removed on cleanup
- [ ] Weak references used where appropriate
- [ ] AbortController used for fetch requests

### Timers
- [ ] All intervals/timeouts cleared
- [ ] No dangling timer references
- [ ] Cleanup in component unmount/destroy

### Closures
- [ ] No large objects captured unnecessarily
- [ ] Weak maps used for caching
- [ ] Explicit nulling of references

### DOM Manipulation
- [ ] Detached nodes properly cleaned
- [ ] Element references cleared
- [ ] Observer patterns properly unsubscribed

### Data Structures
- [ ] Caches have size limits
- [ ] LRU eviction implemented
- [ ] Weak references for optional retention

Memory-Safe Patterns

// Use WeakMap for caching
const cache = new WeakMap();

function processObject(obj) {
    if (cache.has(obj)) {
        return cache.get(obj);
    }

    const result = heavyComputation(obj);
    cache.set(obj, result);
    return result;
}

// AbortController for fetch
const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .catch(err => {
        if (err.name === 'AbortError') {
            console.log('Fetch aborted');
        }
    });

// Cleanup: controller.abort();

Conclusion

Memory leak detection is critical for application stability and performance. By using appropriate profiling tools, implementing automated tests, and following best practices, QA engineers can identify and prevent memory leaks before they impact users. This testing complements other performance testing strategies for comprehensive quality assurance.

Key Takeaways:

  • Profile applications regularly for memory growth
  • Use browser/platform-specific tools for analysis
  • Implement automated memory leak tests in CI/CD
  • Review code for common leak patterns
  • Clean up resources properly (listeners, timers, references)
  • Set memory growth thresholds and alert on violations

Memory leaks are preventable — make memory profiling a standard part of testing.