Skip to content

Memory Leaks

메모리 누수의 원인, 탐지, 예방 방법입니다.

Common Causes

1. Global References

# Bad: accumulates forever
cache = []

def process(data):
    cache.append(data)  # Never cleared!

# Better: limit size
from collections import deque

cache = deque(maxlen=100)

2. Circular References

class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a
# Cycle keeps both alive

3. Closures Capturing Large Objects

# Leak: large object captured
def process_data():
    large = [0] * 1000000

    def get_first():
        return large[0]  # Captures entire list

    return get_first  # Keeps large alive

# Better: capture only what's needed
def process_data():
    large = [0] * 1000000
    first = large[0]

    def get_first():
        return first  # Only captures the value

    return get_first

4. Event Handlers Not Removed

# Leak: handler keeps object alive
class Widget:
    def __init__(self, event_system):
        event_system.subscribe('click', self.handle)

    def handle(self, event):
        pass
    # Widget never collected until unsubscribed

Finalizers (__del__)

__del__은 신뢰할 수 없고 문제를 일으킬 수 있습니다.

Problems with __del__

class Resource:
    def __del__(self):
        self.cleanup()

# May not be called if:
# - Program exits abruptly
# - Circular references exist
# - Exceptions in __del__

Object Resurrection

saved = None

class Item:
    def __del__(self):
        global saved
        saved = self  # Resurrect!

obj = Item()
del obj
print(saved)  # Object still exists!

Best Practices: Avoid __del__

# Instead of __del__, use explicit cleanup
class Resource:
    def close(self):
        # Explicit cleanup
        pass

r = Resource()
try:
    use(r)
finally:
    r.close()

Better: Context Managers

class Resource:
    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.cleanup()

# Guaranteed cleanup
with Resource() as r:
    use(r)

Or: weakref Callbacks

import weakref

def cleanup(ref):
    print("Object collected")

obj = SomeObject()
ref = weakref.ref(obj, cleanup)

# cleanup called on GC (more reliable than __del__)

Detection

1. Track Memory Growth

import tracemalloc

tracemalloc.start()

for i in range(100):
    process()

    if i % 10 == 0:
        current, peak = tracemalloc.get_traced_memory()
        print(f"Current: {current / 10**6:.1f} MB")

tracemalloc.stop()

2. Monitor Object Count

import gc

before = len(gc.get_objects())

process()

after = len(gc.get_objects())
print(f"Objects created: {after - before}")

3. Find Top Allocations

import tracemalloc

tracemalloc.start()

# Run code
process()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

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

GC Performance

Collection Pauses

import gc
import time

start = time.time()
collected = gc.collect()
pause = time.time() - start

print(f"Pause: {pause*1000:.2f}ms")
print(f"Collected: {collected}")

Disable During Critical Sections

import gc

gc.disable()
try:
    # Performance-critical code
    compute()
finally:
    gc.enable()
    gc.collect()

Manual Collection Control

import gc

gc.disable()

for i in range(1000):
    process()

    if i % 100 == 0:
        gc.collect()

gc.enable()

GC Tuning

Adjust Thresholds

import gc

# Default: (700, 10, 10)
# Fewer collections, longer pauses:
gc.set_threshold(10000, 100, 100)

# More collections, shorter pauses:
gc.set_threshold(100, 5, 5)

Reduce Allocations

# Bad: many allocations
result = []
for i in range(1000):
    result.append([i])

# Better: pre-allocate
result = [None] * 1000
for i in range(1000):
    result[i] = i

Monitor GC Impact

import gc

# Before
before = gc.get_count()

process()

# After
after = gc.get_count()
print(f"Gen0 delta: {after[0] - before[0]}")

Prevention Checklist

Strategy Implementation
Limit caches functools.lru_cache(maxsize=128)
Break cycles obj.parent = None
Weak references weakref.WeakValueDictionary()
Context managers with open_resource() as r:
Explicit cleanup try/finally with .close()
Avoid __del__ Use context managers instead

Summary

  • Causes: globals, cycles, closures, event handlers
  • Detection: tracemalloc, gc.get_objects()
  • Prevention: limit caches, break cycles, use weak refs
  • Finalizers: avoid __del__, use context managers
  • Performance: tune thresholds, control collection timing