Deep Dive Into Python Memory Management
In this article, we shall dive deep into the internals of Python to discover how it handles memory management and object creation.
Memory management is the process of allocating and de-allocating memory for creation of objects and in Python, the Python memory manager automatically handles this process under the hood by running periodically to allocate and de-allocate memory when its no longer needed. The memory manager knows when to de-allocate memory used by objects through the process of Reference Counting i.e. the memory manager keeps track of all objects created and the number of variables referring to each memory location, once the reference count of any memory location containing an object gets down to zero, the memory space is destroyed and freed up for use.
Lets talk about how objects are created in Python: when you assign a value to a variable in Python, what actually goes on under the hood is that a memory location (imagine a box) is created and a value e.g. an integer or a string is created at that memory location (put inside the box) and the memory address is then assigned to the variable (imagine a label being slapped onto the side of the box). It is this memory location a variable references whenever it needs to fetch the value of an object.
In Python, the type and memory space for an object are determined and allocated at run-time. On creation, the interpreter creates an object whose type is dictated by the syntax that is used for the operand on the right-hand side of an assignment. After the object is created, a reference to that object is assigned to the variable on the left-hand side of the assignment.
Garbage Collection and Reference Counting
Earlier in the introduction, we mentioned how the Python memory manager creates and destroys objects, well this is done with the garbage collector which works in conjunction with the memory manager and the reference counter. This process is called Garbage Collection and this process runs as your program is running and goes into action as soon as the reference count of a memory location is incremented i.e. a variable references a memory location and decremented i.e. when the reference to an object is reassigned or when the object is explicitly deleted. Python’s garbage collector is actually a combination of reference counting and the periodic invocation of a cyclic garbage collector. When an object’s refcount reaches zero, the interpreter pauses to deallocate it and all objects that were reachable only from that object. In addition to this reference counting, the garbage collector also notices if a large number of objects have been allocated (and not deallocated though reference counting). In such cases, the interpreter will pause to try to clear out any unreferenced cycles.
Lets write some code to explain it more in detail:
Explainer: Under the hood, the memory manager created a memory location, created a string object with a value of “Python Rocks!”, slotted it in that memory location and assigned the memory location to the variable name ‘message’. When we tried to print the message variable with the built in print function, the memory manager retrieved the value of the string located at the memory location the variable message referenced and printed it to the screen. When the message variable was deleted with del, the reference count was decremented and since only one variable was referencing that memory location, as soon as it was deleted, the reference count fell to zero and the garbage collector instantly came into play by de-allocating and freeing up that memory location thereby destroying the string object. This is why subsequent print calls to that variable returned a NameError because we were trying to access a location that no longer existed. The beautiful thing about this is that the whole process is automated and developers do not need to get their hands dirty by handling memory manually.
Although Python handles memory automatically, there are situations where you have to garbage collect objects manually. Example: we know that the Python interpreter keeps track of references to objects in code but there are situations where you have circular references in code i.e. when two variables refer to each other so the reference count never falls to zero so the memory allocated for these objects never gets de-allocated and this could lead to a memory leak. Therefore to solve this reference cycle problem, we have to garbage collect manually using the gc module.
The result variable contains the number of objects it has collected and de-allocated in memory.
The del statement
The del statement removes a single reference to an object. Its syntax is:
del obj1 … objN
Running the del statement will:
Remove a variable name from current namespace
Lower reference count to the reference (by one)
The del statement does not free variables as in C, it simply says that you no longer need it.
The del statement
del_stmt ::= “del” target_list
Deletion is recursively defined very similar to the way assignment is defined. Rather than spelling it out in full details, here are some hints.
Deletion of a target list recursively deletes each target, from left to right. Deletion of a name removes the binding of that name from the local or global namespace, depending on whether the name occurs in a global statement in the same code block. If the nane is unbound, a NameError exception will be raised.
Deletion of attribute references, subscriptions and slicing’s is passed to the primary object involved; deletion of a slicing is in general equivalent to assignment of an empty slice of the right type (but even this is determined by the sliced object).
Kindly note that there is no mention of freeing memory. What happens is that you tell Python that it can do whatever it wants with that memory. In this case your Python implementation stores the memory for later use in a memory cache. This allows Python to run faster by not needing to allocate as much memory later.
Interning is the process by which objects are reused on-demand by the memory manager. As startup, the Cpython interpreter, pre-loads a global list of integers in the range [-5, 256]. So anytime a variable references a number within that range, the interpreter will use a cached version of that integer object in memory.
This is why:
hex(id(a)) == hex(id(b)) → True
where a and b are both referencing a number which falls within the preloaded range of integers and
hex(id(a)) == hex(id(b)) → False
When a and b are both referencing a number which falls outside of the preloaded range of integers. The Python memory manager not only interns integers, it also interns strings that look like variables i.e. short strings like ‘Hello’ and long strings that have no spaces in-between each word that make up the string e.g. “Hello_world_program”.
You can manually intern a long string if it does not have the normal characteristics of a string that the memory manager would normally intern. You can do this by:
What is the use of interning you might ask? Well, in normal day to day programming, you would hardly ever be required to go that low but if you are building for example, a tokenizer or some program that would be working with a large number of strings, then interning would greatly speed up your code.
The Python Global Interpreter Lock (GIL)
The python GIL is a lock that allows only one thread to control the Python interpreter at any given time i.e. only one thread can be executed at once. What exactly does this do you might ask? Well lets return to our explanation of how reference counting in Python is executed; reference count of an object is incremented when:
- It is assigned to a variable
- Passed as an argument during a function invocation
- Passed as an object in an iterable i.e. a list. The reference count of an object is also decremented when it is:
- Explicitly deleted with the del statement
- The variable is assigned another memory location
- The variable goes out of scope i.e. after the function has finished running. Now imagine if the Python GIL did not exist? That would mean more than one thread can be executed in a Python program that means there might be cases where two or more threads might want to increment or decrement the reference count of an object simultaneously which if it happens would lead to either memory leaks due to memory that never gets released or where the memory is released even when the reference count is not zero i.e. a variable still references it. This phenomenon is called a Race Condition and invariables leads to unexpected behavior in code i.e. bugs. So the reference count can be kept safe by adding locks to all data structures that are shared across threads so that they are not modified inconsistently. However, adding a lock to each object or groups of objects means multiple locks will exist which can cause another problem – Deadlocks. And deadlocks can only happen if there is more than one lock on an object). Also, another side effect would be decreased performance caused by the repeated acquisition and release of locks. The GIL is a single lock on the interpreter itself which adds a rule that execution of any Python bytecode requires acquiring the interpreter lock. This prevents deadlocks (as there is only one lock) and does not introduce much performance overhead. The only disadvantage is that it makes CPU-bound programs single threaded. In the end we have been able to see how Python manages memory and object creation with the garbage collector, reference counting and the GIL.
Refcount (Reference Counting) - In computer science, reference counting is a programming technique of storing the number of references, pointers, or handles to a resource, such as an object, a block of memory, disk space, and others. In garbage collection algorithms, reference counts may be used to deallocate objects which are no longer needed. https://en.wikipedia.org/wiki/Reference_counting
Memory Leak - In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released. A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code. https://en.wikipedia.org/wiki/Memory_leak
Deadlock - In concurrent computing, a deadlock is a state in which each member of a group is waiting for another member, including itself, to take action, such as sending a message or more commonly releasing a lock. https://en.wikipedia.org/wiki/Deadlock