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.