Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Zarządzanie pamięcią - garbage collection, memory leaks

Zarządzanie pamięcią w JavaScript jest jednym z najważniejszych aspektów wydajności aplikacji. Mimo że JavaScript posiada automatyczny garbage collector, deweloperzy muszą zrozumieć, jak działa pamięć, aby unikać memory leaks i optymalizować wydajność swoich aplikacji.

Jak działa pamięć w JavaScript

Memory lifecycle

1// Cykl życia pamięci w JavaScript
2const memoryLifecycle = {
3  // 1. Alokacja pamięci - automatyczna
4  allocation: () => {
5    const obj = { name: 'John', age: 30 }; // Alokacja w heap
6    const arr = [1, 2, 3, 4, 5]; // Alokacja w heap
7    const num = 42; // Alokacja w stack (primitive)
8    
9    return { obj, arr, num };
10  },
11  
12  // 2. Użycie pamięci - odczyt/zapis
13  usage: (data) => {
14    data.obj.name = 'Jane'; // Użycie zaalokowanej pamięci
15    data.arr.push(6);
16    
17    console.log(data.obj, data.arr);
18  },
19  
20  // 3. Zwalnianie pamięci - automatyczne przez GC
21  release: () => {
22    // Gdy zmienne wychodzą poza scope, stają się kandydatami do GC
23    // GC uruchomi się automatycznie w odpowiednim momencie
24  }
25};
26
27// Demonstracja cyklu
28function demonstrateMemoryLifecycle() {
29  const data = memoryLifecycle.allocation(); // Alokacja
30  memoryLifecycle.usage(data); // Użycie
31  // Po zakończeniu funkcji, 'data' staje się kandydatem do GC
32}
33
34demonstrateMemoryLifecycle();

Stack vs Heap

1// Stack - przechowuje prymitywy i referencje
2function stackExample() {
3  const a = 5; // Stack
4  const b = 'hello'; // Stack (reference do string w heap)
5  const c = true; // Stack
6  
7  console.log('Stack variables:', a, b, c);
8} // Po zakończeniu funkcji, wszystko ze stack zostaje usunięte
9
10// Heap - przechowuje obiekty i tablice
11function heapExample() {
12  const obj = { x: 1, y: 2 }; // Obiekt w heap, referencja w stack
13  const arr = [1, 2, 3]; // Tablica w heap, referencja w stack
14  
15  // Modyfikacja obiektu w heap
16  obj.z = 3;
17  arr.push(4);
18  
19  return { obj, arr }; // Zwracamy referencje
20} // Referencje ze stack są usuwane, ale obiekty w heap pozostają
21
22// Memory allocation visualization
23class MemoryVisualizer {
24  constructor() {
25    this.allocations = [];
26    this.currentId = 0;
27  }
28  
29  allocate(type, size, description) {
30    const allocation = {
31      id: this.currentId++,
32      type: type, // 'stack' | 'heap'
33      size: size,
34      description: description,
35      timestamp: Date.now(),
36      freed: false
37    };
38    
39    this.allocations.push(allocation);
40    console.log(`🔧 Allocated ${type}: ${description} (${size} bytes)`);
41    
42    return allocation.id;
43  }
44  
45  free(id) {
46    const allocation = this.allocations.find(a => a.id === id);
47    if (allocation) {
48      allocation.freed = true;
49      allocation.freedAt = Date.now();
50      console.log(`🗑️ Freed: ${allocation.description}`);
51    }
52  }
53  
54  getMemoryStats() {
55    const active = this.allocations.filter(a => !a.freed);
56    const freed = this.allocations.filter(a => a.freed);
57    
58    return {
59      totalAllocations: this.allocations.length,
60      activeAllocations: active.length,
61      freedAllocations: freed.length,
62      totalActiveSize: active.reduce((sum, a) => sum + a.size, 0),
63      memoryLeaks: active.filter(a => Date.now() - a.timestamp > 30000) // > 30s
64    };
65  }
66}
67
68const memoryViz = new MemoryVisualizer();
69
70// Przykład użycia
71function demonstrateMemoryAllocation() {
72  // Stack allocations
73  const stackId1 = memoryViz.allocate('stack', 8, 'number variable');
74  const stackId2 = memoryViz.allocate('stack', 8, 'string reference');
75  
76  // Heap allocations
77  const heapId1 = memoryViz.allocate('heap', 64, 'object {name, age, city}');
78  const heapId2 = memoryViz.allocate('heap', 32, 'array [1,2,3,4,5]');
79  
80  // Symulacja zwalniania pamięci
81  setTimeout(() => {
82    memoryViz.free(stackId1);
83    memoryViz.free(stackId2);
84    console.log('Memory stats:', memoryViz.getMemoryStats());
85  }, 1000);
86  
87  setTimeout(() => {
88    memoryViz.free(heapId1);
89    // heapId2 nie zostaje zwolniony - symulacja memory leak
90    console.log('Final memory stats:', memoryViz.getMemoryStats());
91  }, 2000);
92}

Garbage Collection w JavaScript

Typy algorytmów GC

1// 1. Reference Counting (stary algorytm)
2class ReferenceCountingDemo {
3  constructor() {
4    this.objects = new Map();
5  }
6  
7  createObject(id, data) {
8    const obj = {
9      id: id,
10      data: data,
11      refCount: 1,
12      references: []
13    };
14    
15    this.objects.set(id, obj);
16    console.log(`Created object ${id} with ref count: 1`);
17    return obj;
18  }
19  
20  addReference(fromId, toId) {
21    const fromObj = this.objects.get(fromId);
22    const toObj = this.objects.get(toId);
23    
24    if (fromObj && toObj) {
25      fromObj.references.push(toId);
26      toObj.refCount++;
27      console.log(`Object ${toId} ref count increased to: ${toObj.refCount}`);
28    }
29  }
30  
31  removeReference(fromId, toId) {
32    const fromObj = this.objects.get(fromId);
33    const toObj = this.objects.get(toId);
34    
35    if (fromObj && toObj) {
36      const index = fromObj.references.indexOf(toId);
37      if (index > -1) {
38        fromObj.references.splice(index, 1);
39        toObj.refCount--;
40        console.log(`Object ${toId} ref count decreased to: ${toObj.refCount}`);
41        
42        // Auto GC when ref count reaches 0
43        if (toObj.refCount === 0) {
44          this.collectObject(toId);
45        }
46      }
47    }
48  }
49  
50  collectObject(id) {
51    const obj = this.objects.get(id);
52    if (obj) {
53      // Recursively decrease reference counts
54      obj.references.forEach(refId => {
55        this.removeReference(id, refId);
56      });
57      
58      this.objects.delete(id);
59      console.log(`🗑️ Collected object ${id}`);
60    }
61  }
62  
63  // Problem: Circular references
64  createCircularReference() {
65    const obj1 = this.createObject('circular1', 'First object');
66    const obj2 = this.createObject('circular2', 'Second object');
67    
68    // Wzajemne referencje - memory leak w reference counting!
69    this.addReference('circular1', 'circular2');
70    this.addReference('circular2', 'circular1');
71    
72    console.log('⚠️ Created circular reference - will never be collected!');
73    
74    return { obj1, obj2 };
75  }
76}
77
78// 2. Mark and Sweep (nowoczesny algorytm)
79class MarkAndSweepDemo {
80  constructor() {
81    this.objects = new Map();
82    this.roots = new Set(); // Global variables, stack references
83  }
84  
85  createObject(id, data) {
86    const obj = {
87      id: id,
88      data: data,
89      references: [],
90      marked: false
91    };
92    
93    this.objects.set(id, obj);
94    return obj;
95  }
96  
97  addToRoots(id) {
98    this.roots.add(id);
99    console.log(`Added ${id} to GC roots`);
100  }
101  
102  removeFromRoots(id) {
103    this.roots.delete(id);
104    console.log(`Removed ${id} from GC roots`);
105  }
106  
107  addReference(fromId, toId) {
108    const fromObj = this.objects.get(fromId);
109    if (fromObj && !fromObj.references.includes(toId)) {
110      fromObj.references.push(toId);
111    }
112  }
113  
114  // Mark phase - oznacza wszystkie osiągnalne obiekty
115  markPhase() {
116    console.log('🏷️ Starting mark phase...');
117    
118    // Wyczyść poprzednie oznaczenia
119    this.objects.forEach(obj => obj.marked = false);
120    
121    // Oznacz wszystkie obiekty osiągnalne z roots
122    const visited = new Set();
123    
124    const markRecursive = (id) => {
125      if (visited.has(id)) return;
126      visited.add(id);
127      
128      const obj = this.objects.get(id);
129      if (obj) {
130        obj.marked = true;
131        console.log(`  Marked object ${id}`);
132        
133        // Rekurencyjnie oznacz referencje
134        obj.references.forEach(refId => markRecursive(refId));
135      }
136    };
137    
138    // Rozpocznij od wszystkich roots
139    this.roots.forEach(rootId => markRecursive(rootId));
140  }
141  
142  // Sweep phase - usuwa nieoznaczone obiekty
143  sweepPhase() {
144    console.log('🧹 Starting sweep phase...');
145    
146    const toDelete = [];
147    
148    this.objects.forEach((obj, id) => {
149      if (!obj.marked) {
150        toDelete.push(id);
151      }
152    });
153    
154    toDelete.forEach(id => {
155      console.log(`🗑️ Collecting object ${id}`);
156      this.objects.delete(id);
157    });
158    
159    console.log(`Collected ${toDelete.length} objects`);
160  }
161  
162  // Pełny cykl GC
163  runGarbageCollection() {
164    console.log('🔄 Running garbage collection...');
165    this.markPhase();
166    this.sweepPhase();
167    console.log(`Active objects: ${this.objects.size}`);
168  }
169  
170  // Demonstracja rozwiązywania circular references
171  demonstrateCircularReferences() {
172    // Tworzenie obiektów z circular references
173    const obj1 = this.createObject('mark1', 'Object 1');
174    const obj2 = this.createObject('mark2', 'Object 2');
175    const obj3 = this.createObject('mark3', 'Object 3');
176    
177    // Circular references
178    this.addReference('mark1', 'mark2');
179    this.addReference('mark2', 'mark3');
180    this.addReference('mark3', 'mark1'); // Circular!
181    
182    // Tylko obj1 jest w roots
183    this.addToRoots('mark1');
184    
185    console.log('Before GC - objects with circular references');
186    this.runGarbageCollection(); // Wszystkie będą oznaczone jako osiągalne
187    
188    // Usuwamy z roots
189    this.removeFromRoots('mark1');
190    
191    console.log('After removing from roots');
192    this.runGarbageCollection(); // Wszystkie będą zebrane mimo circular references!
193  }
194}

Generational Garbage Collection

1// Nowoczesne silniki JS używają generational GC
2class GenerationalGCDemo {
3  constructor() {
4    // Różne generacje obiektów
5    this.youngGeneration = new Map(); // Nowe obiekty
6    this.oldGeneration = new Map();   // Stare obiekty
7    this.permanentGeneration = new Map(); // Bardzo stare obiekty
8    
9    this.gcCycles = 0;
10    this.promotionThreshold = 3; // Ile cykli GC przeżyć aby zostać promowanym
11  }
12  
13  allocateObject(id, data, size = 64) {
14    const obj = {
15      id: id,
16      data: data,
17      size: size,
18      generation: 'young',
19      gcSurvived: 0,
20      allocatedAt: Date.now(),
21      lastAccessed: Date.now()
22    };
23    
24    this.youngGeneration.set(id, obj);
25    console.log(`📦 Allocated ${id} in young generation`);
26    return obj;
27  }
28  
29  accessObject(id) {
30    let obj = this.youngGeneration.get(id) || 
31              this.oldGeneration.get(id) || 
32              this.permanentGeneration.get(id);
33    
34    if (obj) {
35      obj.lastAccessed = Date.now();
36      console.log(`📖 Accessed object ${id}`);
37    }
38    
39    return obj;
40  }
41  
42  // Minor GC - tylko young generation
43  runMinorGC() {
44    console.log('🔄 Running Minor GC (young generation)...');
45    this.gcCycles++;
46    
47    const survivors = [];
48    const toPromote = [];
49    
50    this.youngGeneration.forEach((obj, id) => {
51      // Symulacja: obiekty używane w ostatnich 5 sekundach przeżywają
52      const timeSinceLastAccess = Date.now() - obj.lastAccessed;
53      
54      if (timeSinceLastAccess < 5000) {
55        obj.gcSurvived++;
56        survivors.push({ id, obj });
57        
58        // Promocja do old generation
59        if (obj.gcSurvived >= this.promotionThreshold) {
60          toPromote.push({ id, obj });
61        }
62        
63        console.log(`✅ Object ${id} survived (count: ${obj.gcSurvived})`);
64      } else {
65        console.log(`🗑️ Collected young object ${id}`);
66      }
67    });
68    
69    // Wyczyść young generation
70    this.youngGeneration.clear();
71    
72    // Przywróć survivors
73    survivors.forEach(({ id, obj }) => {
74      if (!toPromote.find(p => p.id === id)) {
75        this.youngGeneration.set(id, obj);
76      }
77    });
78    
79    // Promuj do old generation
80    toPromote.forEach(({ id, obj }) => {
81      obj.generation = 'old';
82      this.oldGeneration.set(id, obj);
83      console.log(`⬆️ Promoted ${id} to old generation`);
84    });
85    
86    console.log(`Minor GC completed. Young: ${this.youngGeneration.size}, Old: ${this.oldGeneration.size}`);
87  }
88  
89  // Major GC - wszystkie generacje
90  runMajorGC() {
91    console.log('🔄 Running Major GC (all generations)...');
92    
93    const collectFromGeneration = (generation, name) => {
94      const survivors = new Map();
95      let collected = 0;
96      
97      generation.forEach((obj, id) => {
98        const timeSinceLastAccess = Date.now() - obj.lastAccessed;
99        
100        // Old generation ma dłuższy czas życia
101        const threshold = name === 'old' ? 10000 : 5000;
102        
103        if (timeSinceLastAccess < threshold) {
104          survivors.set(id, obj);
105        } else {
106          collected++;
107          console.log(`🗑️ Collected ${name} object ${id}`);
108        }
109      });
110      
111      generation.clear();
112      survivors.forEach((obj, id) => generation.set(id, obj));
113      
114      return collected;
115    };
116    
117    const youngCollected = collectFromGeneration(this.youngGeneration, 'young');
118    const oldCollected = collectFromGeneration(this.oldGeneration, 'old');
119    
120    console.log(`Major GC completed. Collected ${youngCollected + oldCollected} objects`);
121    console.log(`Remaining - Young: ${this.youngGeneration.size}, Old: ${this.oldGeneration.size}`);
122  }
123  
124  // Automatyczny GC na podstawie heurystyk
125  autoGC() {
126    const youngSize = this.youngGeneration.size;
127    const oldSize = this.oldGeneration.size;
128    
129    // Triggers dla GC
130    if (youngSize > 100) { // Dużo młodych obiektów
131      this.runMinorGC();
132    }
133    
134    if (oldSize > 50 || this.gcCycles % 10 === 0) { // Dużo starych obiektów lub co 10 cykli
135      this.runMajorGC();
136    }
137  }
138  
139  getGenerationStats() {
140    return {
141      young: this.youngGeneration.size,
142      old: this.oldGeneration.size,
143      permanent: this.permanentGeneration.size,
144      totalGCCycles: this.gcCycles
145    };
146  }
147}
148
149// Demonstracja generational GC
150const genGC = new GenerationalGCDemo();
151
152// Symulacja allocation patterns
153function simulateAllocationPattern() {
154  // Tworzenie wielu krótkotrwałych obiektów
155  for (let i = 0; i < 50; i++) {
156    genGC.allocateObject(`temp_${i}`, `Temporary object ${i}`);
157  }
158  
159  // Tworzenie kilku długotrwałych obiektów
160  for (let i = 0; i < 5; i++) {
161    const longLived = genGC.allocateObject(`long_${i}`, `Long-lived object ${i}`);
162    
163    // Symulacja regularnego dostępu
164    setInterval(() => {
165      genGC.accessObject(`long_${i}`);
166    }, 1000);
167  }
168  
169  // Automatyczny GC co sekundę
170  setInterval(() => {
171    genGC.autoGC();
172    console.log('Generation stats:', genGC.getGenerationStats());
173  }, 2000);
174}

Rodzaje Memory Leaks

1. Global Variables

1// Problem: Niekontrolowane globalne zmienne
2class GlobalVariableLeaks {
3  static demonstrateLeaks() {
4    // BAD: Przypadkowe globalne zmienne
5    function accidentalGlobal() {
6      // Brak 'var', 'let', 'const' - tworzy globalną zmienną!
7      leakedVariable = 'This will leak!';
8      
9      // Też globalnie przez 'this' w non-strict mode
10      this.anotherLeak = { data: new Array(1000).fill('leak') };
11    }
12    
13    accidentalGlobal();
14    
15    // Te zmienne pozostaną w pamięci do końca działania aplikacji
16    console.log('Leaked variables:', window.leakedVariable, window.anotherLeak);
17  }
18  
19  static demonstrateSolutions() {
20    // GOOD: Używaj strict mode
21    'use strict';
22    
23    function properFunction() {
24      // Błąd kompilacji zamiast globalnej zmiennej
25      // notDeclared = 'This will throw error in strict mode';
26      
27      let properVariable = 'This is properly scoped';
28      const anotherProper = { data: 'safe' };
29      
30      return { properVariable, anotherProper };
31    }
32    
33    // GOOD: Moduł pattern
34    const safeModule = (function() {
35      let privateData = [];
36      
37      return {
38        addData: function(item) {
39          privateData.push(item);
40        },
41        
42        getData: function() {
43          return privateData.slice(); // Return copy
44        },
45        
46        clear: function() {
47          privateData = [];
48        }
49      };
50    })();
51    
52    return { safeModule };
53  }
54}
55
56// 2. Event Listeners
57class EventListenerLeaks {
58  constructor() {
59    this.data = new Array(10000).fill('memory consuming data');
60    this.handlers = new Map();
61  }
62  
63  // BAD: Event listeners bez cleanup
64  addLeakyListeners() {
65    const button = document.getElementById('leaky-button');
66    
67    // Problem: listener referencje obiekt, który może mieć dużo danych
68    const handler = () => {
69      console.log('Clicked!', this.data.length);
70    };
71    
72    button?.addEventListener('click', handler);
73    
74    // Kiedy komponent jest usuwany, listener pozostaje!
75    // Cały obiekt nie może być zebrany przez GC
76  }
77  
78  // GOOD: Proper cleanup
79  addProperListeners() {
80    const button = document.getElementById('proper-button');
81    
82    const handler = (event) => {
83      console.log('Proper click!', event.target.id);
84    };
85    
86    this.handlers.set('button-click', handler);
87    button?.addEventListener('click', handler);
88  }
89  
90  cleanup() {
91    // Zawsze usuń event listeners podczas cleanup
92    this.handlers.forEach((handler, key) => {
93      if (key === 'button-click') {
94        const button = document.getElementById('proper-button');
95        button?.removeEventListener('click', handler);
96      }
97    });
98    
99    this.handlers.clear();
100    console.log('Event listeners cleaned up');
101  }
102  
103  // Modern approach: AbortController
104  addModernListeners() {
105    const controller = new AbortController();
106    const signal = controller.signal;
107    
108    document.addEventListener('click', (event) => {
109      console.log('Modern click!', event.target);
110    }, { signal });
111    
112    // Cleanup wszystkich listeners na raz
113    setTimeout(() => {
114      controller.abort();
115      console.log('All listeners aborted');
116    }, 10000);
117  }
118}
119
120// 3. Timers i Intervals
121class TimerLeaks {
122  constructor() {
123    this.data = new Array(100000).fill('timer data');
124    this.timers = new Set();
125  }
126  
127  // BAD: Timers bez cleanup
128  createLeakyTimers() {
129    // Problem: setInterval referencje całą klasę
130    const intervalId = setInterval(() => {
131      console.log('Leaky timer tick', this.data.length);
132    }, 1000);
133    
134    // Timer nigdy nie zostaje wyczyszczony!
135    // Obiekt nie może być zebrany przez GC
136    
137    return intervalId;
138  }
139  
140  // GOOD: Tracked timers
141  createTrackedTimers() {
142    const intervalId = setInterval(() => {
143      console.log('Tracked timer tick');
144    }, 1000);
145    
146    this.timers.add(intervalId);
147    
148    // Auto cleanup po czasie
149    const timeoutId = setTimeout(() => {
150      this.clearTimer(intervalId);
151    }, 10000);
152    
153    this.timers.add(timeoutId);
154    
155    return intervalId;
156  }
157  
158  clearTimer(timerId) {
159    clearInterval(timerId);
160    clearTimeout(timerId);
161    this.timers.delete(timerId);
162    console.log(`Timer ${timerId} cleared`);
163  }
164  
165  cleanup() {
166    this.timers.forEach(timerId => {
167      clearInterval(timerId);
168      clearTimeout(timerId);
169    });
170    
171    this.timers.clear();
172    console.log('All timers cleaned up');
173  }
174  
175  // Modern approach: AbortController dla async operations
176  createAbortableTimer() {
177    const controller = new AbortController();
178    
179    const timer = {
180      start: () => {
181        const interval = setInterval(() => {
182          if (controller.signal.aborted) {
183            clearInterval(interval);
184            return;
185          }
186          
187          console.log('Abortable timer tick');
188        }, 1000);
189        
190        return interval;
191      },
192      
193      stop: () => {
194        controller.abort();
195      }
196    };
197    
198    return timer;
199  }
200}
201
202// 4. Closures
203class ClosureLeaks {
204  static demonstrateLeaks() {
205    function createLeakyClosures() {
206      const largeData = new Array(1000000).fill('leak data');
207      
208      // Problem: closure trzyma referencję do całego scope
209      return {
210        // Nawet jeśli używamy tylko małej części danych
211        getSmallPart: function() {
212          return largeData.slice(0, 10);
213        },
214        
215        // Ta funkcja też trzyma całe largeData w pamięci
216        getLength: function() {
217          return largeData.length;
218        }
219      };
220    }
221    
222    const leaky = createLeakyClosures();
223    console.log('Small part:', leaky.getSmallPart());
224    
225    // largeData pozostaje w pamięci mimo że potrzebujemy tylko length!
226    return leaky;
227  }
228  
229  static demonstrateSolutions() {
230    function createOptimizedClosures() {
231      const largeData = new Array(1000000).fill('optimized data');
232      
233      // GOOD: Wyciągnij tylko potrzebne dane
234      const smallPart = largeData.slice(0, 10);
235      const dataLength = largeData.length;
236      
237      // largeData może być zebrany przez GC
238      
239      return {
240        getSmallPart: function() {
241          return smallPart;
242        },
243        
244        getLength: function() {
245          return dataLength;
246        }
247      };
248    }
249    
250    // GOOD: Null references when done
251    function createCleanClosures() {
252      let data = new Array(100000).fill('clean data');
253      
254      const api = {
255        process: function() {
256          const result = data.map(item => item.toUpperCase());
257          data = null; // Explicit cleanup!
258          return result;
259        }
260      };
261      
262      return api;
263    }
264    
265    return { createOptimizedClosures, createCleanClosures };
266  }
267}
268
269// 5. DOM References
270class DOMReferenceLeaks {
271  constructor() {
272    this.domReferences = new Map();
273    this.observers = [];
274  }
275  
276  // BAD: Holding DOM references
277  createDOMLeaks() {
278    // Problem: referencje do usuniętych elementów DOM
279    const elements = document.querySelectorAll('.dynamic-content');
280    
281    elements.forEach((element, index) => {
282      this.domReferences.set(`element_${index}`, {
283        node: element, // Direct reference!
284        data: new Array(1000).fill(`data_${index}`)
285      });
286    });
287    
288    // Nawet jeśli elementy zostaną usunięte z DOM,
289    // pozostaną w pamięci przez te referencje!
290  }
291  
292  // GOOD: WeakMap dla DOM references
293  createSafeDOMReferences() {
294    const elementData = new WeakMap();
295    
296    const elements = document.querySelectorAll('.safe-content');
297    
298    elements.forEach((element, index) => {
299      // WeakMap pozwala na GC elementu gdy zostanie usunięty z DOM
300      elementData.set(element, {
301        id: index,
302        data: new Array(1000).fill(`safe_data_${index}`)
303      });
304    });
305    
306    return elementData;
307  }
308  
309  // Proper cleanup dla DOM observers
310  setupDOMObserver() {
311    const observer = new MutationObserver((mutations) => {
312      mutations.forEach(mutation => {
313        console.log('DOM changed:', mutation.type);
314      });
315    });
316    
317    observer.observe(document.body, {
318      childList: true,
319      subtree: true
320    });
321    
322    this.observers.push(observer);
323    
324    // Auto cleanup
325    setTimeout(() => {
326      this.cleanupObservers();
327    }, 30000);
328  }
329  
330  cleanupObservers() {
331    this.observers.forEach(observer => {
332      observer.disconnect();
333    });
334    
335    this.observers = [];
336    console.log('DOM observers cleaned up');
337  }
338  
339  cleanup() {
340    this.domReferences.clear();
341    this.cleanupObservers();
342  }
343}

Memory Leak Detection i Monitoring

1. Performance API dla Memory Monitoring

1// Memory monitoring utilities
2class MemoryMonitor {
3  constructor() {
4    this.measurements = [];
5    this.isMonitoring = false;
6    this.alertThreshold = 50 * 1024 * 1024; // 50MB
7  }
8  
9  startMonitoring(interval = 5000) {
10    if (this.isMonitoring) return;
11    
12    this.isMonitoring = true;
13    console.log('🔍 Started memory monitoring');
14    
15    const monitor = () => {
16      if (!this.isMonitoring) return;
17      
18      const memInfo = this.getMemoryInfo();
19      this.measurements.push({
20        ...memInfo,
21        timestamp: Date.now()
22      });
23      
24      this.checkMemoryAlerts(memInfo);
25      this.trimMeasurements();
26      
27      setTimeout(monitor, interval);
28    };
29    
30    monitor();
31  }
32  
33  stopMonitoring() {
34    this.isMonitoring = false;
35    console.log('⏹️ Stopped memory monitoring');
36  }
37  
38  getMemoryInfo() {
39    if (performance.memory) {
40      return {
41        usedJSHeapSize: performance.memory.usedJSHeapSize,
42        totalJSHeapSize: performance.memory.totalJSHeapSize,
43        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
44        usedPercent: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100
45      };
46    }
47    
48    return {
49      usedJSHeapSize: 0,
50      totalJSHeapSize: 0,
51      jsHeapSizeLimit: 0,
52      usedPercent: 0
53    };
54  }
55  
56  checkMemoryAlerts(memInfo) {
57    if (memInfo.usedJSHeapSize > this.alertThreshold) {
58      console.warn('⚠️ High memory usage detected:', {
59        used: this.formatBytes(memInfo.usedJSHeapSize),
60        percent: memInfo.usedPercent.toFixed(2) + '%'
61      });
62      
63      this.triggerMemoryCleanup();
64    }
65    
66    // Check for memory leaks (constant growth)
67    if (this.measurements.length > 10) {
68      const recent = this.measurements.slice(-10);
69      const growth = recent[recent.length - 1].usedJSHeapSize - recent[0].usedJSHeapSize;
70      const timeSpan = recent[recent.length - 1].timestamp - recent[0].timestamp;
71      const growthRate = growth / timeSpan; // bytes per ms
72      
73      if (growthRate > 1000) { // > 1KB/s growth
74        console.warn('📈 Potential memory leak detected:', {
75          growthRate: this.formatBytes(growthRate * 1000) + '/s',
76          totalGrowth: this.formatBytes(growth)
77        });
78      }
79    }
80  }
81  
82  triggerMemoryCleanup() {
83    // Force garbage collection if available (dev tools)
84    if (window.gc) {
85      window.gc();
86      console.log('🗑️ Forced garbage collection');
87    }
88    
89    // Cleanup event
90    window.dispatchEvent(new CustomEvent('memoryCleanup', {
91      detail: { memoryUsage: this.getMemoryInfo() }
92    }));
93  }
94  
95  generateMemoryReport() {
96    if (this.measurements.length === 0) {
97      return { error: 'No measurements available' };
98    }
99    
100    const latest = this.measurements[this.measurements.length - 1];
101    const oldest = this.measurements[0];
102    
103    return {
104      currentUsage: {
105        used: this.formatBytes(latest.usedJSHeapSize),
106        total: this.formatBytes(latest.totalJSHeapSize),
107        limit: this.formatBytes(latest.jsHeapSizeLimit),
108        percent: latest.usedPercent.toFixed(2) + '%'
109      },
110      trend: {
111        totalGrowth: this.formatBytes(latest.usedJSHeapSize - oldest.usedJSHeapSize),
112        timeSpan: this.formatTime(latest.timestamp - oldest.timestamp),
113        measurementCount: this.measurements.length
114      },
115      recommendations: this.generateRecommendations()
116    };
117  }
118  
119  generateRecommendations() {
120    const recommendations = [];
121    const latest = this.measurements[this.measurements.length - 1];
122    
123    if (latest.usedPercent > 80) {
124      recommendations.push('Memory usage is very high - consider implementing cleanup strategies');
125    }
126    
127    if (this.measurements.length > 5) {
128      const recentGrowth = this.measurements.slice(-5);
129      const avgGrowth = recentGrowth.reduce((sum, m, i) => 
130        i === 0 ? 0 : sum + (m.usedJSHeapSize - recentGrowth[i-1].usedJSHeapSize), 0
131      ) / (recentGrowth.length - 1);
132      
133      if (avgGrowth > 1024 * 1024) { // > 1MB average growth
134        recommendations.push('Consistent memory growth detected - check for memory leaks');
135      }
136    }
137    
138    return recommendations;
139  }
140  
141  formatBytes(bytes) {
142    const units = ['B', 'KB', 'MB', 'GB'];
143    let size = bytes;
144    let unitIndex = 0;
145    
146    while (size >= 1024 && unitIndex < units.length - 1) {
147      size /= 1024;
148      unitIndex++;
149    }
150    
151    return `${size.toFixed(2)} ${units[unitIndex]}`;
152  }
153  
154  formatTime(ms) {
155    const seconds = Math.floor(ms / 1000);
156    const minutes = Math.floor(seconds / 60);
157    const hours = Math.floor(minutes / 60);
158    
159    if (hours > 0) return `${hours}h ${minutes % 60}m`;
160    if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
161    return `${seconds}s`;
162  }
163  
164  trimMeasurements() {
165    // Keep only last 100 measurements
166    if (this.measurements.length > 100) {
167      this.measurements = this.measurements.slice(-100);
168    }
169  }
170}
171
172// Memory leak detector
173class MemoryLeakDetector {
174  constructor() {
175    this.snapshots = [];
176    this.leakPatterns = new Map();
177  }
178  
179  takeSnapshot(label = 'snapshot') {
180    const snapshot = {
181      label: label,
182      timestamp: Date.now(),
183      memory: performance.memory ? {
184        used: performance.memory.usedJSHeapSize,
185        total: performance.memory.totalJSHeapSize
186      } : null,
187      objectCounts: this.countObjects()
188    };
189    
190    this.snapshots.push(snapshot);
191    console.log(`📸 Memory snapshot taken: ${label}`);
192    
193    return snapshot;
194  }
195  
196  countObjects() {
197    // Przybliżone liczenie obiektów (w rzeczywistości użyj dev tools)
198    const counts = {
199      arrays: 0,
200      objects: 0,
201      functions: 0,
202      strings: 0
203    };
204    
205    // Symulacja - w prawdziwej implementacji użyj heap snapshot API
206    return counts;
207  }
208  
209  compareSnapshots(snapshot1Label, snapshot2Label) {
210    const snap1 = this.snapshots.find(s => s.label === snapshot1Label);
211    const snap2 = this.snapshots.find(s => s.label === snapshot2Label);
212    
213    if (!snap1 || !snap2) {
214      console.error('Snapshots not found');
215      return null;
216    }
217    
218    const comparison = {
219      memoryGrowth: snap2.memory ? snap2.memory.used - snap1.memory.used : 0,
220      timeSpan: snap2.timestamp - snap1.timestamp,
221      objectGrowth: {},
222      suspiciousGrowth: []
223    };
224    
225    // Analiza wzrostu obiektów
226    Object.keys(snap1.objectCounts).forEach(type => {
227      const growth = snap2.objectCounts[type] - snap1.objectCounts[type];
228      comparison.objectGrowth[type] = growth;
229      
230      if (growth > 100) { // Suspicious growth threshold
231        comparison.suspiciousGrowth.push({
232          type: type,
233          growth: growth,
234          growthRate: growth / (comparison.timeSpan / 1000) // per second
235        });
236      }
237    });
238    
239    return comparison;
240  }
241  
242  detectLeakPatterns() {
243    if (this.snapshots.length < 3) return [];
244    
245    const patterns = [];
246    const recent = this.snapshots.slice(-3);
247    
248    // Check for consistent memory growth
249    const memoryGrowth = recent.map((snap, i) => 
250      i === 0 ? 0 : snap.memory.used - recent[i-1].memory.used
251    ).slice(1);
252    
253    const consistentGrowth = memoryGrowth.every(growth => growth > 0);
254    
255    if (consistentGrowth) {
256      const avgGrowth = memoryGrowth.reduce((a, b) => a + b) / memoryGrowth.length;
257      
258      patterns.push({
259        type: 'consistent_memory_growth',
260        severity: avgGrowth > 1024 * 1024 ? 'high' : 'medium',
261        description: `Memory consistently growing by ${(avgGrowth / 1024 / 1024).toFixed(2)}MB per measurement`,
262        avgGrowth: avgGrowth
263      });
264    }
265    
266    return patterns;
267  }
268}
269
270// Global memory monitor instance
271const memoryMonitor = new MemoryMonitor();
272const leakDetector = new MemoryLeakDetector();
273
274// Usage example
275function startMemoryAnalysis() {
276  memoryMonitor.startMonitoring(2000); // Every 2 seconds
277  
278  // Take initial snapshot
279  leakDetector.takeSnapshot('initial');
280  
281  // Simulate some operations and take snapshots
282  setTimeout(() => {
283    // Simulate memory allocation
284    const data = new Array(100000).fill('test data');
285    leakDetector.takeSnapshot('after_allocation');
286  }, 5000);
287  
288  setTimeout(() => {
289    // Generate memory report
290    const report = memoryMonitor.generateMemoryReport();
291    console.log('Memory Report:', report);
292    
293    const patterns = leakDetector.detectLeakPatterns();
294    console.log('Leak Patterns:', patterns);
295  }, 10000);
296}

Best Practices dla Memory Management

1. Proactive Memory Management

1// Memory-aware component design
2class MemoryAwareComponent {
3  constructor(config) {
4    this.config = config;
5    this.resources = new Map();
6    this.cleanup = new Set();
7    this.memoryBudget = config.memoryBudget || 10 * 1024 * 1024; // 10MB default
8  }
9  
10  // Resource tracking
11  allocateResource(key, factory, size = 0) {
12    if (this.getCurrentMemoryUsage() + size > this.memoryBudget) {
13      this.performCleanup();
14    }
15    
16    const resource = factory();
17    this.resources.set(key, {
18      data: resource,
19      size: size,
20      allocatedAt: Date.now(),
21      lastAccessed: Date.now()
22    });
23    
24    return resource;
25  }
26  
27  getResource(key) {
28    const resource = this.resources.get(key);
29    if (resource) {
30      resource.lastAccessed = Date.now();
31      return resource.data;
32    }
33    return null;
34  }
35  
36  getCurrentMemoryUsage() {
37    return Array.from(this.resources.values())
38      .reduce((total, resource) => total + resource.size, 0);
39  }
40  
41  performCleanup() {
42    console.log('🧹 Performing memory cleanup...');
43    
44    const now = Date.now();
45    const staleThreshold = 5 * 60 * 1000; // 5 minutes
46    
47    // Remove stale resources
48    for (const [key, resource] of this.resources) {
49      if (now - resource.lastAccessed > staleThreshold) {
50        this.deallocateResource(key);
51      }
52    }
53    
54    // Run custom cleanup functions
55    this.cleanup.forEach(cleanupFn => {
56      try {
57        cleanupFn();
58      } catch (error) {
59        console.error('Cleanup function failed:', error);
60      }
61    });
62  }
63  
64  deallocateResource(key) {
65    const resource = this.resources.get(key);
66    if (resource) {
67      // Custom cleanup if resource has cleanup method
68      if (resource.data && typeof resource.data.cleanup === 'function') {
69        resource.data.cleanup();
70      }
71      
72      this.resources.delete(key);
73      console.log(`🗑️ Deallocated resource: ${key}`);
74    }
75  }
76  
77  addCleanupFunction(fn) {
78    this.cleanup.add(fn);
79  }
80  
81  removeCleanupFunction(fn) {
82    this.cleanup.delete(fn);
83  }
84  
85  destroy() {
86    // Cleanup all resources
87    this.resources.forEach((_, key) => {
88      this.deallocateResource(key);
89    });
90    
91    // Run all cleanup functions
92    this.cleanup.forEach(cleanupFn => {
93      try {
94        cleanupFn();
95      } catch (error) {
96        console.error('Cleanup function failed during destroy:', error);
97      }
98    });
99    
100    this.cleanup.clear();
101    console.log('Component destroyed and cleaned up');
102  }
103}
104
105// Object pooling dla często używanych obiektów
106class ObjectPool {
107  constructor(factory, resetFn, initialSize = 10) {
108    this.factory = factory;
109    this.resetFn = resetFn;
110    this.pool = [];
111    this.activeObjects = new Set();
112    
113    // Pre-allocate objects
114    for (let i = 0; i < initialSize; i++) {
115      this.pool.push(this.factory());
116    }
117  }
118  
119  acquire() {
120    let obj;
121    
122    if (this.pool.length > 0) {
123      obj = this.pool.pop();
124    } else {
125      obj = this.factory();
126      console.log('🏭 Created new object (pool exhausted)');
127    }
128    
129    this.activeObjects.add(obj);
130    return obj;
131  }
132  
133  release(obj) {
134    if (this.activeObjects.has(obj)) {
135      this.activeObjects.delete(obj);
136      
137      // Reset object to initial state
138      if (this.resetFn) {
139        this.resetFn(obj);
140      }
141      
142      this.pool.push(obj);
143    }
144  }
145  
146  getStats() {
147    return {
148      poolSize: this.pool.length,
149      activeObjects: this.activeObjects.size,
150      totalObjects: this.pool.length + this.activeObjects.size
151    };
152  }
153}
154
155// Przykład użycia object pooling
156const vectorPool = new ObjectPool(
157  () => ({ x: 0, y: 0, z: 0 }), // factory
158  (vector) => { vector.x = 0; vector.y = 0; vector.z = 0; }, // reset
159  50 // initial size
160);
161
162function performVectorCalculations() {
163  const vectors = [];
164  
165  // Acquire vectors from pool instead of creating new ones
166  for (let i = 0; i < 100; i++) {
167    const vector = vectorPool.acquire();
168    vector.x = Math.random();
169    vector.y = Math.random();
170    vector.z = Math.random();
171    vectors.push(vector);
172  }
173  
174  // Do calculations...
175  
176  // Release vectors back to pool
177  vectors.forEach(vector => vectorPool.release(vector));
178  
179  console.log('Vector pool stats:', vectorPool.getStats());
180}
181
182// WeakRef dla optional caching
183class WeakRefCache {
184  constructor() {
185    this.cache = new Map();
186    this.finalizationRegistry = new FinalizationRegistry((key) => {
187      console.log(`🗑️ Object with key '${key}' was garbage collected`);
188      this.cache.delete(key);
189    });
190  }
191  
192  set(key, value) {
193    const weakRef = new WeakRef(value);
194    this.cache.set(key, weakRef);
195    this.finalizationRegistry.register(value, key);
196  }
197  
198  get(key) {
199    const weakRef = this.cache.get(key);
200    if (!weakRef) return null;
201    
202    const value = weakRef.deref();
203    if (value === undefined) {
204      // Object was garbage collected
205      this.cache.delete(key);
206      return null;
207    }
208    
209    return value;
210  }
211  
212  has(key) {
213    return this.get(key) !== null;
214  }
215  
216  delete(key) {
217    this.cache.delete(key);
218  }
219  
220  size() {
221    // Clean up dead references
222    for (const [key, weakRef] of this.cache) {
223      if (weakRef.deref() === undefined) {
224        this.cache.delete(key);
225      }
226    }
227    
228    return this.cache.size;
229  }
230}

2. Memory Profiling Tools

1// Development memory profiling utilities
2class MemoryProfiler {
3  constructor() {
4    this.profiles = new Map();
5    this.isProfilingEnabled = process.env.NODE_ENV === 'development';
6  }
7  
8  startProfile(name) {
9    if (!this.isProfilingEnabled) return;
10    
11    const profile = {
12      name: name,
13      startTime: performance.now(),
14      startMemory: performance.memory ? {
15        used: performance.memory.usedJSHeapSize,
16        total: performance.memory.totalJSHeapSize
17      } : null,
18      snapshots: []
19    };
20    
21    this.profiles.set(name, profile);
22    console.log(`🔍 Started memory profile: ${name}`);
23  }
24  
25  snapshot(profileName, label) {
26    if (!this.isProfilingEnabled) return;
27    
28    const profile = this.profiles.get(profileName);
29    if (!profile) return;
30    
31    const snapshot = {
32      label: label,
33      timestamp: performance.now(),
34      memory: performance.memory ? {
35        used: performance.memory.usedJSHeapSize,
36        total: performance.memory.totalJSHeapSize
37      } : null
38    };
39    
40    profile.snapshots.push(snapshot);
41    console.log(`📸 Memory snapshot: ${profileName}.${label}`);
42  }
43  
44  endProfile(name) {
45    if (!this.isProfilingEnabled) return;
46    
47    const profile = this.profiles.get(name);
48    if (!profile) return;
49    
50    profile.endTime = performance.now();
51    profile.endMemory = performance.memory ? {
52      used: performance.memory.usedJSHeapSize,
53      total: performance.memory.totalJSHeapSize
54    } : null;
55    
56    const report = this.generateProfileReport(profile);
57    console.log(`📊 Memory profile completed: ${name}`, report);
58    
59    return report;
60  }
61  
62  generateProfileReport(profile) {
63    const duration = profile.endTime - profile.startTime;
64    
65    let memoryGrowth = 0;
66    if (profile.startMemory && profile.endMemory) {
67      memoryGrowth = profile.endMemory.used - profile.startMemory.used;
68    }
69    
70    const snapshots = profile.snapshots.map((snapshot, index) => {
71      const prevSnapshot = index > 0 ? profile.snapshots[index - 1] : 
72        { memory: profile.startMemory, timestamp: profile.startTime };
73      
74      return {
75        label: snapshot.label,
76        timeDelta: snapshot.timestamp - prevSnapshot.timestamp,
77        memoryDelta: snapshot.memory && prevSnapshot.memory ? 
78          snapshot.memory.used - prevSnapshot.memory.used : 0
79      };
80    });
81    
82    return {
83      duration: duration.toFixed(2) + 'ms',
84      memoryGrowth: memoryGrowth,
85      memoryGrowthFormatted: this.formatBytes(memoryGrowth),
86      snapshots: snapshots,
87      recommendations: this.generateProfileRecommendations(profile, memoryGrowth)
88    };
89  }
90  
91  generateProfileRecommendations(profile, memoryGrowth) {
92    const recommendations = [];
93    
94    if (memoryGrowth > 1024 * 1024) { // > 1MB growth
95      recommendations.push('Significant memory growth detected - review object allocations');
96    }
97    
98    if (profile.snapshots.length > 0) {
99      const biggestGrowth = Math.max(...profile.snapshots.map((s, i) => {
100        const prev = i > 0 ? profile.snapshots[i-1] : { memory: profile.startMemory };
101        return s.memory && prev.memory ? s.memory.used - prev.memory.used : 0;
102      }));
103      
104      if (biggestGrowth > 512 * 1024) { // > 512KB single growth
105        recommendations.push('Large single allocation detected - consider chunking or streaming');
106      }
107    }
108    
109    return recommendations;
110  }
111  
112  formatBytes(bytes) {
113    if (bytes === 0) return '0 B';
114    const k = 1024;
115    const sizes = ['B', 'KB', 'MB', 'GB'];
116    const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
117    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
118  }
119}
120
121// Global profiler instance
122const memoryProfiler = new MemoryProfiler();
123
124// Decorator dla automatycznego profilowania funkcji
125function profileMemory(name) {
126  return function(target, propertyKey, descriptor) {
127    const originalMethod = descriptor.value;
128    
129    descriptor.value = function(...args) {
130      const profileName = name || `${target.constructor.name}.${propertyKey}`;
131      
132      memoryProfiler.startProfile(profileName);
133      
134      try {
135        const result = originalMethod.apply(this, args);
136        
137        if (result instanceof Promise) {
138          return result.finally(() => {
139            memoryProfiler.endProfile(profileName);
140          });
141        } else {
142          memoryProfiler.endProfile(profileName);
143          return result;
144        }
145      } catch (error) {
146        memoryProfiler.endProfile(profileName);
147        throw error;
148      }
149    };
150    
151    return descriptor;
152  };
153}
154
155// Usage example
156class DataProcessor {
157  @profileMemory('processLargeDataset')
158  processLargeDataset(data) {
159    memoryProfiler.snapshot('processLargeDataset', 'start');
160    
161    // Process data in chunks
162    const chunks = this.chunkArray(data, 1000);
163    const results = [];
164    
165    chunks.forEach((chunk, index) => {
166      memoryProfiler.snapshot('processLargeDataset', `chunk_${index}`);
167      
168      const processed = chunk.map(item => this.transformItem(item));
169      results.push(...processed);
170    });
171    
172    memoryProfiler.snapshot('processLargeDataset', 'end');
173    return results;
174  }
175  
176  chunkArray(array, size) {
177    const chunks = [];
178    for (let i = 0; i < array.length; i += size) {
179      chunks.push(array.slice(i, i + size));
180    }
181    return chunks;
182  }
183  
184  transformItem(item) {
185    return { ...item, processed: true, timestamp: Date.now() };
186  }
187}

Podsumowanie

Zarządzanie pamięcią w JavaScript wymaga:

  1. Zrozumienia Garbage Collection - jak działa i kiedy się uruchamia
  2. Świadomości Memory Leaks - główne przyczyny i sposoby unikania
  3. Proaktywnego podejścia - monitoring i cleanup
  4. Właściwych wzorców - object pooling, WeakMap, AbortController
  5. Narzędzi diagnostycznych - profiling i monitoring

Kluczowe zasady:

  • Zawsze sprzątaj po sobie (event listeners, timers, observers)
  • Używaj WeakMap/WeakSet dla opcjonalnych referencji
  • Monitoruj zużycie pamięci w aplikacjach produkcyjnych
  • Implementuj object pooling dla często alokowanych obiektów
  • Unikaj przypadkowych zmiennych globalnych
  • Testuj aplikację pod kątem memory leaks

Właściwe zarządzanie pamięcią to klucz do wydajnych, skalowalnych aplikacji JavaScript.

Ir a CodeWorlds