Memory Management and Garbage Collection
About 1241 wordsAbout 16 min
2025-08-05
This section explores JavaScript's memory management model, garbage collection mechanisms, and techniques for writing memory-efficient code.
Memory Management Fundamentals
JavaScript is a garbage-collected language, meaning developers don't manually allocate or deallocate memory. However, understanding how memory works is crucial for writing efficient applications.
Memory Lifecycle:
- Allocation: Memory is allocated when you create variables, objects, functions, etc.
- Usage: Reading and writing values in allocated memory
- Release: Memory is freed when no longer needed (handled by garbage collector)
The Call Stack and Heap
JavaScript uses two main memory areas:
Call Stack
- Stores primitive values and references to objects
- Organized as a LIFO (Last In, First Out) structure
- Each function call creates a new stack frame
- Automatically managed when functions return
Heap
- Stores objects, functions, and complex data structures
- Unstructured memory region
- Managed by the garbage collector
- Larger and less organized than the stack
// Stack allocation (primitives)
let a = 10; // Stored in stack
let b = "hello"; // Stored in stack
// Heap allocation (objects)
let obj = { // Object stored in heap
name: "John", // Reference stored in stack
age: 30
};
let arr = [1, 2, 3]; // Array stored in heap
Garbage Collection Algorithms
JavaScript engines use sophisticated garbage collection algorithms, primarily based on mark-and-sweep:
Mark-and-Sweep Algorithm
- Mark Phase: The garbage collector starts from root objects (global objects, current function scope) and marks all reachable objects
- Sweep Phase: Removes all unmarked (unreachable) objects from memory
// Example of reachable vs unreachable objects
let user = { name: "John", age: 30 };
let admin = user; // Two references to the same object
user = null; // Object still reachable via admin
admin = null; // Object is now unreachable - eligible for garbage collection
Generational Collection
Modern JavaScript engines use generational garbage collection:
Young Generation:
- New objects are allocated here
- Small and frequently collected
- Uses a "scavenge" algorithm (copy collection)
Old Generation:
- Objects that survive multiple young generation collections
- Larger and less frequently collected
- Uses mark-and-sweep or mark-compact algorithms
// Objects that survive multiple GC cycles move to old generation
function longLivedObject() {
const data = new Array(1000).fill('data');
return () => data; // Closure keeps data alive
}
const closure = longLivedObject();
// The data array will likely be promoted to old generation
Memory Leaks and Prevention
Memory leaks occur when objects are no longer needed but remain reachable in memory.
Common Memory Leak Patterns
1. Accidental Global Variables:
function leakGlobal() {
leakedVar = "This is now global"; // No 'let', 'const', or 'var'
}
leakGlobal();
// leakedVar is now a global variable and won't be garbage collected
2. Forgotten Timers and Intervals:
function startTimer() {
const data = new Array(1000000).fill('data');
setInterval(() => {
console.log('Timer running, data kept alive:', data.length);
}, 1000);
}
// Timer keeps reference to data, preventing garbage collection
// Solution: Store timer reference and clear it when done
3. Closures:
function createClosure() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log('Closure has access to largeData');
};
}
const closure = createClosure();
// largeData remains in memory as long as closure exists
4. Event Listeners:
function setupElement() {
const element = document.getElementById('myElement');
const data = new Array(1000000).fill('data');
element.addEventListener('click', function() {
console.log('Element clicked, data:', data.length);
});
// Element and data will remain in memory
// Solution: Remove event listener when done
}
Memory Leak Prevention Techniques
1. Proper Cleanup:
class ResourceManager {
constructor() {
this.timers = new Set();
this.listeners = new Set();
this.data = new Map();
}
addTimer(callback, delay) {
const timerId = setInterval(callback, delay);
this.timers.add(timerId);
return timerId;
}
addListener(element, event, handler) {
element.addEventListener(event, handler);
this.listeners.add({ element, event, handler });
}
cleanup() {
// Clear all timers
this.timers.forEach(timerId => clearInterval(timerId));
this.timers.clear();
// Remove all event listeners
this.listeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.listeners.clear();
// Clear data
this.data.clear();
}
}
2. Weak References:
// WeakMap - keys are weakly referenced
const weakMap = new WeakMap();
let obj = { name: "John" };
weakMap.set(obj, "some data");
obj = null; // Key can now be garbage collected
// WeakSet - values are weakly referenced
const weakSet = new WeakSet();
let obj2 = { name: "Jane" };
weakSet.add(obj2);
obj2 = null; // Value can now be garbage collected
Memory Optimization Techniques
1. Object Pooling
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.inUse = new Set();
// Pre-allocate objects
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
acquire() {
let obj = this.pool.pop() || this.createFn();
this.inUse.add(obj);
return obj;
}
release(obj) {
if (this.inUse.has(obj)) {
this.inUse.delete(obj);
this.resetFn(obj);
this.pool.push(obj);
}
}
}
// Usage for temporary objects
const vectorPool = new ObjectPool(
() => ({ x: 0, y: 0, z: 0 }),
(v) => { v.x = v.y = v.z = 0; }
);
function processVectors() {
const v1 = vectorPool.acquire();
const v2 = vectorPool.acquire();
// Use vectors
v1.x = 10; v1.y = 20; v1.z = 30;
v2.x = 5; v2.y = 15; v2.z = 25;
// Release back to pool
vectorPool.release(v1);
vectorPool.release(v2);
}
2. Lazy Loading
class LazyLoader {
constructor(dataLoader) {
this.dataLoader = dataLoader;
this.cachedData = null;
this.loaded = false;
}
getData() {
if (!this.loaded) {
this.cachedData = this.dataLoader();
this.loaded = true;
}
return this.cachedData;
}
clearCache() {
this.cachedData = null;
this.loaded = false;
}
}
// Usage
const lazyData = new LazyLoader(() => {
return new Array(1000000).fill('expensive data');
});
// Data only loaded when first accessed
const data = lazyData.getData();
3. Memory Profiling
// Memory usage monitoring
function monitorMemory() {
const used = process.memoryUsage();
console.log('Memory Usage:');
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
}
// In browser environments
function getMemoryUsage() {
if (performance.memory) {
console.log('Memory Usage:');
console.log(` Used: ${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)} MB`);
console.log(` Total: ${Math.round(performance.memory.totalJSHeapSize / 1024 / 1024)} MB`);
console.log(` Limit: ${Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)} MB`);
}
}
Advanced Memory Concepts
1. WeakRef and FinalizationRegistry
// WeakRef - weak reference to an object
let obj = { data: "important" };
const weakRef = new WeakRef(obj);
// Later, check if object is still alive
const data = weakRef.deref();
if (data) {
console.log('Object still exists:', data);
} else {
console.log('Object was garbage collected');
}
obj = null; // Object can now be garbage collected
// FinalizationRegistry - cleanup when objects are garbage collected
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with ID ${heldValue} was garbage collected`);
});
function createObject(id) {
const obj = { id, data: "important" };
registry.register(obj, id);
return obj;
}
const myObj = createObject(123);
myObj = null; // When garbage collected, registry will log the cleanup
2. Memory Layout and Optimization
// Hidden Classes and Inline Caching (V8 Engine)
// Optimized object creation with consistent property order
function createOptimizedPoint(x, y) {
return { x, y }; // Consistent property order helps optimization
}
// vs. inconsistent property order (less optimized)
function createUnoptimizedPoint(x, y) {
const point = {};
if (Math.random() > 0.5) {
point.x = x;
point.y = y;
} else {
point.y = y;
point.x = x;
}
return point;
}
// Arrays: Use typed arrays for numeric data when possible
const regularArray = new Array(1000);
const int32Array = new Int32Array(1000); // More memory efficient
const float64Array = new Float64Array(1000); // More memory efficient
3. Large Object Management
// Chunked processing for large datasets
class ChunkedProcessor {
constructor(data, chunkSize = 1000) {
this.data = data;
this.chunkSize = chunkSize;
this.currentIndex = 0;
}
processChunk(processorFn) {
const chunk = this.data.slice(this.currentIndex, this.currentIndex + this.chunkSize);
const result = processorFn(chunk);
this.currentIndex += this.chunkSize;
return { result, done: this.currentIndex >= this.data.length };
}
reset() {
this.currentIndex = 0;
}
}
// Usage
const largeData = new Array(1000000).fill(0).map((_, i) => i);
const processor = new ChunkedProcessor(largeData, 10000);
function processChunk() {
const { result, done } = processor.processChunk(chunk => {
return chunk.reduce((sum, val) => sum + val, 0);
});
if (!done) {
setTimeout(processChunk, 0); // Process next chunk in next event loop cycle
}
}
processChunk();
Changelog
2aa48
-web-deploy(Auto): Update base URL for web-pages branchon
Copyright
Copyright Ownership:WARREN Y.F. LONG
License under:Attribution-NonCommercial-NoDerivatives 4.0 International (CC-BY-NC-ND-4.0)