As developers, we strive to create applications that are not only functional and user-friendly, but also optimized to utilize system resources effectively. Proper memory management is crucial for optimizing resources, enhancing performance, and conserving system resources.
What’s a memory leak?
A memory leak refers to a situation in which allocated memory is not properly deallocated or released when it is no longer needed. As a result, the program continues to consume memory resources unnecessarily. Over time, if memory leaks persist, the program’s memory usage can grow, leading to performance degradation, increased memory consumption, and potentially causing the program to crash or exhaust the available memory.
Manual memory management
In some languages, like C and C++ developers are responsible for manually allocating and deallocating memory using functions like malloc()
(for memory allocation) and free()
(to release memory). If memory is not freed properly, it can result in memory leaks.
Memory leaks in this context occur when dynamically allocated memory is not properly deallocated or released when it is no longer needed.
#include <stdio.h> #include <stdlib.h> void performOperations() { int* data = malloc(sizeof(int)); // Some operations on 'data'... // Memory is not freed } int main() { performOperations(); return 0; }
To prevent memory leaks in C, it is important to ensure that for every memory allocation, there is a corresponding deallocation. Careful tracking of allocated memory and releasing it when it is no longer needed is crucial.
In this example, memory is allocated dynamically using malloc()
to store an integer. However, the developer forgets to release the memory using free()
. As a result, each time performOperations()
is called, memory will be allocated but never freed, leading to a memory leak.
Now let’s see how this could be avoided:
#include <stdio.h> #include <stdlib.h> void performOperations() { int* data = malloc(sizeof(int)); // Some operations on 'data'... free(data); // Memory is properly freed } int main() { performOperations(); return 0; }
In this modified example, the allocated memory is freed within the performOperations()
function before it ends, preventing a memory leak and ensuring proper memory management.
To prevent memory leaks in C, it is important to ensure that for every memory allocation, there is a corresponding deallocation. Careful tracking of allocated memory and releasing it when it is no longer needed is crucial.
Garbage Collection
Garbage collection is a memory management technique used in some programming languages to automatically reclaim memory that is no longer in use by the program.
In languages with garbage collection, such as Java, Kotlin, C#, Python, and many others, the runtime environment includes a garbage collector. The garbage collector periodically identifies and collects objects that are no longer reachable or referenced by the program, freeing up their associated memory.
One advantage of garbage collection is that it automatically manages memory deallocation, reducing the risk of memory leaks and making memory management more convenient for developers. It eliminates the need for explicit calls to deallocation functions like free()
in C.
Let’s look at this Kotlin code:
fun performOperations() { val list = mutableListOf<String>() repeat(1000000) { list.add("Element $it") } // No explicit memory deallocation needed // Some other operations... // The 'list' object is no longer reachable // Garbage collection will automatically reclaim its memory } fun main() { performOperations() // Other main function code... }
Job Offers
In this example, when the performOperations()
function finishes executing, the list
object goes out of scope and becomes unreachable. The garbage collector will identify that the memory occupied by the list
object is no longer needed and automatically reclaim that memory.
How is a memory leak created when we have garbage collection ?
In order to release memory, the garbage collector first traverses the object graph, starting from known root objects (such as global variables, local variables in the call stack, and static variables) and marks all objects that are still reachable.
Any objects that are not marked are considered unreachable and eligible for memory reclamation. Then the memory occupied by those unreachable objects is released.
Example with Singleton object
As you may already know, singletons hold a static reference to their own instance, ensuring that only one instance exists throughout the application. This static reference prevents the singleton instance from becoming unreachable, even if it is no longer actively used by the program.
While the garbage collector can reclaim memory for objects that are no longer reachable, singletons, due to their static references, are considered reachable and the responsibility of properly managing their lifecycle and memory usage falls on the developer.
One of the common errors I have seen as an Android developer is holding a reference to an activity in a Singleton.
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.myButton).setOnClickListener { val intent = Intent(this,SecondActivity::class.java) startActivity(intent) } val secondView = MySingleton.sharedView /* spme other stuffs */ } }
class SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) val textView = findViewById<TextView>(R.id.secondActivity) MySingleton.sharedView = textView } override fun onDestroy() { super.onDestroy() } } object MySingleton { var sharedView: View? = null }
Android activities are managed by the Android system and can be garbage collected when they are no longer needed. The activity remains in memory as long as it is visible to the user or in the foreground. However, when an activity is no longer needed or goes through its lifecycle states, the system may decide to garbage collect it to free up memory resources.
In this example, MySingleton
is holding a reference to the textView
in SecondActivity
, and it is not cleared when SecondActivity
is destroyed.
When we’ll navigate back to the MainActivity
, it will cause a memory leak.
It is important to ensure that singletons are cleared or released when they are no longer needed, following appropriate application-specific logic or lifecycle management strategies.
class SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) val textView = findViewById<TextView>(R.id.secondActivity) MySingleton.sharedView = textView } override fun onDestroy() { super.onDestroy() MySingleton.sharedView = null } }
We can solve that just by clearing the reference to the view from the singleton, in the onDestroy
method.
There are several ways of introducing memory leaks into a program, and garbage collection may not be able to deallocate certain types of resources other than memory, such as file handles or database connections.
In such cases, developers still need to manually release these resources to avoid resource leaks.
Conclusion
Identifying and fixing memory leaks is crucial for maintaining a healthy and efficient application. The significant challenge lies in the unpredictability of memory leaks, as they often remain hidden until the application eventually crashes. In an upcoming article, we will delve deeper into how to detect and fix memory leaks on Android.
You should also read
This article was previously published on proandroiddev.com