Topic: 6 Deeply Understand Memory Leaks and Garbage Collection
Hello devs, In our previous blog, you saw words like Memory leaks and garbage collection. What is this and how much is important in our app development? Everything we discussed in this blog. Alright, devs let's dive into the new topic from the series Learn Android from Basic to Advanced.
This blog is important because Memory leaks and garbage collection are extremely important concepts in Android development, as they directly impact the performance and stability of your application.
Memory Leaks
A Memory leak occurs When the memory is allocated for an object in your application and that memory is no longer needed, but the reference to that object is not properly released, it leads to a memory leak and prevents the garbage collector from reclaiming the memory they occupy.
The reason behind Memory Leaks and how we can resolve
Non-Static Inner Classes
Problem: Non-static inner classes can hold implicit references to the outer class, preventing it from being garbage collected.
public class MemoryLeakExample { private List<EventListener> listeners = new ArrayList<>(); public void addListener(EventListener listener) { listeners.add(listener); } public void removeListener(EventListener listener) { listeners.remove(listener); } interface EventListener { void onEvent(); } public class InnerListener implements EventListener { @Override public void onEvent() { // Handle event } } public static void main(String[] args) { MemoryLeakExample example = new MemoryLeakExample(); example.addListener(example.new InnerListener()); // Do something // Forget to remove listener } }
In the above code, we have a
MemoryLeakExample
class which has a non-static inner classInnerListener
. An instance ofInnerListener
is added to thelisteners
list in theMemoryLeakExample
. However, if you forget to remove the listener when it's no longer needed, it can lead to a memory leak because theInnerListener
instance holds a reference to the outer class instance (MemoryLeakExample
), preventing it from being garbage collected.Solution: Convert non-static inner classes to static inner classes or use a WeakReference to avoid holding strong references.
public class MemoryLeakExample { private List<EventListener> listeners = new ArrayList<>(); public void addListener(EventListener listener) { listeners.add(listener); } public void removeListener(EventListener listener) { listeners.remove(listener); } interface EventListener { void onEvent(); } public static class InnerListener implements EventListener { @Override public void onEvent() { // Handle event } } public static void main(String[] args) { MemoryLeakExample example = new MemoryLeakExample(); example.addListener(new InnerListener()); // Do something // No need to remove listener explicitly } }
In this solution,
InnerListener
is a static inner class. Therefore, it doesn't hold a reference to the outer class instance, eliminating the possibility of a memory leak.Unclosed Resources
Problem: Failing to close resources such as databases, cursors, or file streams can lead to memory leaks.
import java.io.BufferedReader import java.io.FileReader import java.io.IOException class ResourceHandler { fun readFromFile(fileName: String): String { val reader = BufferedReader(FileReader(fileName)) return reader.readLine() } }
In this code:
We have a
ResourceHandler
class that contains a methodreadFromFile
for reading from a file.Inside the
readFromFile
method, aBufferedReader
is created to read from the specified file.However, the
BufferedReader
is not closed after reading, which can lead to resource leaks, especially if this method is called multiple times or if theResourceHandler
object is long-lived.
Solution: Always close resources in a finally block or use try-with-resources constructs to ensure they are released properly.
import java.io.BufferedReader
import java.io.FileReader
import java.io.IOException
class ResourceHandler {
fun readFromFile(fileName: String): String {
var reader: BufferedReader? = null
try {
reader = BufferedReader(FileReader(fileName))
return reader.readLine()
} catch (e: IOException) {
// Handle IOException
return ""
} finally {
try {
reader?.close()
} catch (e: IOException) {
// Handle IOException while closing the reader
}
}
}
}
In this modified code:
We use a
try-finally
block to ensure that theBufferedReader
is closed even if an exception occurs while reading from the file.Inside the
finally
block, we close theBufferedReader
using theclose()
method.This ensures proper resource management and prevents memory leaks caused by unclosed resources.
Static References
Problem: Holding references to activities or contexts using static variables can prevent garbage collection and lead to memory leaks.
class MySingleton { companion object { var context: Context? = null } }
In this code:
We have a singleton class
MySingleton
with a companion object containing a static variablecontext
.This
context
variable can hold a reference to an activity or context.If the activity or context referenced by
context
is destroyed (e.g., due to configuration changes or finishing the activity), but the reference is still held by the static variable, it can lead to memory leaks because the activity or context cannot be garbage collected.
Solution: It's important to avoid holding references to activities or contexts using static variables. If you need to access the context within a class, consider passing it as a parameter or using dependency injection frameworks like Dagger or Koin.
class MySingleton {
companion object {
private var weakContext: WeakReference<Context>? = null
fun setContext(context: Context) {
weakContext = WeakReference(context)
}
fun getContext(): Context? {
return weakContext?.get()
}
}
}
In this modified code:
We use a
WeakReference
to hold the reference to the context instead of a direct reference.This allows the activity or context to be garbage collected if there are no other strong references to it.
We provide
setContext
andgetContext
methods to set and get the context respectively, ensuring proper management of the context reference.
Long-lived Callbacks
Problem: Registering callbacks without proper unregistration can result in memory leaks.
class Button { private var clickListener: ClickListener? = null fun setClickListener(listener: ClickListener) { this.clickListener = listener } interface ClickListener { fun onClick() } } class Activity { private val button = Button() fun onCreate() { button.setClickListener(object : Button.ClickListener { override fun onClick() { println("Button clicked") } }) } // onDestroy method is not called, leading to potential memory leak } fun main() { val activity = Activity() activity.onCreate() // Activity instance is not destroyed properly }
In this example:
We have a
Button
class that allows setting aClickListener
callback.The
Activity
class registers a click listener for the button in itsonCreate
method.However, the
onDestroy
method, where the click listener should be unregistered, is not implemented. This can lead to a memory leak because theActivity
instance holds a reference to the anonymous inner class implementing theClickListener
interface.When the
Activity
instance is no longer needed, it might not be garbage collected because it's still referenced by theButton
instance.
Solution: Unregister callbacks in appropriate lifecycle methods (e.g., onDestroy for activities) to release references when they are no longer needed.
class Activity {
private val button = Button()
fun onCreate() {
button.setClickListener(object : Button.ClickListener {
override fun onClick() {
println("Button clicked")
}
})
}
fun onDestroy() {
button.setClickListener(null)
}
}
In this modified code, we add an onDestroy
method to the Activity
class, where we unregister the click listener by passing null
to the setClickListener
method of the button. This ensures that the Activity
instance can be properly garbage collected when it's no longer needed, preventing memory leaks.
Bitmaps and Images
Problem: Loading large bitmaps without recycling them can lead to memory leaks.
import android.graphics.Bitmap import android.graphics.BitmapFactory import java.io.InputStream class ImageLoader { fun loadBitmap(inputStream: InputStream): Bitmap { return BitmapFactory.decodeStream(inputStream) } } fun main() { val imageLoader = ImageLoader() // Assuming this input stream is obtained from a large image file val inputStream = // Get input stream from a large image file val bitmap = imageLoader.loadBitmap(inputStream) // Do something with the bitmap, but forget to recycle it }
In this example:
We have an
ImageLoader
class with aloadBitmap
function that loads a bitmap from an input stream usingBitmapFactory.decodeStream
.In the
main
function, we load a bitmap using theloadBitmap
function.However, after using the bitmap, we forget to call
bitmap.recycle()
. This can lead to memory leaks because bitmaps consume a significant amount of memory, and if not recycled, they can exhaust the available memory over time, especially if large bitmaps are loaded frequently.
Solution: Use image-loading libraries like Picasso or Glide. If loading bitmaps directly, recycle them when they are no longer needed and consider using smaller resolutions.
import android.content.Context
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
class ImageLoader {
companion object {
fun loadImage(context: Context, url: String, imageView: ImageView) {
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.NONE) // Disable disk cache to avoid memory issues
.skipMemoryCache(true) // Skip memory cache to avoid memory issues
.into(imageView)
}
}
}
fun main() {
// Usage example in an Android Activity or Fragment
// imageView is assumed to be the ImageView where the image will be loaded
val context = MyActivity() // Replace MyActivity with your Activity or Fragment instance
val imageView = ImageView(context)
val imageUrl = "https://example.com/image.jpg"
ImageLoader.loadImage(context, imageUrl, imageView)
}
// Example of an Activity class
class MyActivity : Context() {
// Your activity implementation
}
In this code:
ImageLoader
is a helper class containing a static methodloadImage
that uses Glide library to load an image from a URL into anImageView
.Glide.with(context)
initializes Glide with the given context..load(url)
specifies the URL of the image to be loaded..diskCacheStrategy(DiskCacheStrategy.NONE)
and.skipMemoryCache(true)
are used to disable disk and memory caching respectively. This can be important for preventing memory issues when loading large images..into(imageView)
specifies the target ImageView where the image will be loaded.
Using libraries like Glide or Picasso is recommended for loading images in Android applications because they handle many optimizations, including memory management, caching, and asynchronous loading, which can help prevent memory leaks and improve performance.
Custom Views
Problem: Custom views may hold strong references to activities or contexts.
import android.content.Context import android.util.AttributeSet import android.widget.TextView class CustomView(context: Context, attrs: AttributeSet) : TextView(context, attrs) { // Some code here init { // Assuming the custom view needs access to the context or activity // This initialization holds a strong reference to the context or activity // which can cause memory leaks if the context or activity is not properly released initializeView(context) } private fun initializeView(context: Context) { // Do something with the context } }
In this code:
CustomView
is a custom view that extendsTextView
.In the
init
block, the custom view initializes itself and holds a strong reference to the context provided.This strong reference can cause memory leaks if the custom view outlives the activity or context to which it holds a reference.
Solution: Use weak references in custom views or clear references appropriately during the view's lifecycle.
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import java.lang.ref.WeakReference
class CustomView(context: Context, attrs: AttributeSet) : TextView(context, attrs) {
// Some code here
init {
// Assuming the custom view needs access to the context or activity
// Using a weak reference to hold the context reference
initializeView(context)
}
private fun initializeView(context: Context) {
val weakContext = WeakReference(context)
// Do something with the weakContext
}
}
In this modified code:
We use a
WeakReference
to hold the reference to the context.This ensures that the custom view doesn't prevent the activity or context from being garbage collected when it's no longer needed, thus preventing potential memory leaks.
Leaked Fragments
Problem: Fragments not properly detached or removed can result in memory leaks.
import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment class MyFragment : Fragment() { // Some code here override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_my, container, false) } }
In this code:
We have a
MyFragment
class that extendsFragment
.Inside
onCreateView
, the fragment inflates its layout.If this fragment is not properly detached or removed from its parent activity when it's no longer needed, it can hold a reference to the activity, causing a memory leak.
Solution : Use FragmentTransaction to add, replace, or remove fragments. Detach or remove fragments in the appropriate lifecycle methods.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
class MyFragment : Fragment() {
// Some code here
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_my, container, false)
}
override fun onDestroyView() {
super.onDestroyView()
// If needed, perform any cleanup here
detachFragment()
}
private fun detachFragment() {
val fragmentManager: FragmentManager = requireFragmentManager()
fragmentManager.beginTransaction().remove(this).commit()
// Alternatively, you can use fragmentManager.beginTransaction().detach(this).commit()
}
}
In this code:
We override the
onDestroyView
method, which is called when the fragment's view is about to be destroyed.Inside this method, we call
detachFragment()
to detach or remove the fragment from its parent activity.The
detachFragment()
function usesFragmentTransaction
to remove the fragment from its parent activity, ensuring that it doesn't hold a reference to the activity after it's no longer needed, thus preventing memory leaks.
Garbage Collection
Garbage collection is a crucial concept in programming, particularly in languages like Java and Kotlin, which are used for Android development. garbage collection refers to the automatic process of reclaiming memory occupied by objects that are no longer in use by the program. In languages like Java and Kotlin, memory management is handled automatically by the runtime environment, which includes a garbage collector.
Here's a brief overview of how garbage collection works:
Allocation: When you create objects in your Android application, memory is allocated to store those objects. This memory allocation is managed by the JVM (Java Virtual Machine) or the Kotlin runtime environment.
Usage: Your program utilizes these objects as needed during its execution.
Reference tracking: The garbage collector continuously tracks references to objects. An object is considered reachable if it's referenced by any part of your program. If an object becomes unreachable (i.e., there are no references to it), it is a candidate for garbage collection.
Garbage collection: Periodically, or when the system detects low memory conditions, the garbage collector runs. It identifies and removes objects that are no longer reachable from memory, freeing up space for new objects.
Memory reclamation: Once the garbage collector identifies unreachable objects, it reclaims the memory occupied by those objects. This process involves compacting memory, which can help in reducing fragmentation and optimizing memory usage.
So devs As an Android developer, it's essential to understand how garbage collection works because inefficient memory management can lead to performance issues such as excessive memory usage, increased CPU consumption, and even app crashes. You may also need to be aware of strategies to optimize memory usage, such as minimizing object creation, avoiding memory leaks by managing object references carefully, and understanding the impact of different data structures and design patterns on memory consumption.
Overall, garbage collection is a critical aspect of memory management in Android development, and understanding how it works can help you write more efficient and reliable applications. Ok, let's check this by one example.
fun main() {
// Creating an object
var obj1 = MyClass()
// Creating another object and assigning it to obj1
var obj2 = MyClass()
obj1 = obj2 // obj1 now references the same object as obj2
// Creating a third object
var obj3 = MyClass()
// Making obj2 point to null, making the object it referenced unreachable
obj2 = null
// At this point, the object originally referenced by obj1 is unreachable
// Triggering garbage collection (Note: Explicit triggering is not possible in Kotlin)
// Creating a large number of objects to simulate memory pressure
repeat(1000000) {
MyClass()
}
// Garbage collection might occur here if memory pressure is detected
println("Garbage collection might have occurred...")
}
class MyClass {
// Just a dummy class for illustration
}
In this example:
We create three instances of
MyClass
:obj1
,obj2
, andobj3
.Initially,
obj1
andobj2
reference different objects.Later, we make
obj1
reference the same object asobj2
, effectively making the original object referenced byobj1
unreachable.We then set
obj2
tonull
, making the object it referenced unreachable as well.At this point, the objects originally referenced by
obj1
andobj2
are unreachable and eligible for garbage collection.Finally, we simulate memory pressure by creating a large number of objects using
repeat
. This may trigger garbage collection if memory pressure is detected.
Remember devs that in a real-world Android application, you won't explicitly trigger garbage collection as it's handled automatically by the Kotlin runtime environment or the underlying JVM. However, understanding the process helps you write more memory-efficient code.
It's time to wrap up this topic and end this blog post. I hope you found it helpful so far. I will be back soon with the next topic in this series where we can discuss Custom View Class. Looking forward to catching up with you then!
Connect with Me:
Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? π You can also follow my Instagram page for more mobile development-related content. π²π¨βπ» Letβs stay connected, share knowledge and have some fun in the exciting world of app development! π
Subscribe to my newsletter
Read articles from Mayursinh Parmar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mayursinh Parmar
Mayursinh Parmar
π±Mobile App Developer | Android & Flutter ππ‘ Passionate about creating intuitive and engaging apps πβ¨ Letβs shape the future of mobile technology!