Topic: 6 Deeply Understand Memory Leaks and Garbage Collection

Mayursinh ParmarMayursinh Parmar
13 min read

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

  1. 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 class InnerListener. An instance of InnerListener is added to the listeners list in the MemoryLeakExample. However, if you forget to remove the listener when it's no longer needed, it can lead to a memory leak because the InnerListener 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.

  2. 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 method readFromFile for reading from a file.

    • Inside the readFromFile method, a BufferedReader 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 the ResourceHandler 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 the BufferedReader is closed even if an exception occurs while reading from the file.

  • Inside the finally block, we close the BufferedReader using the close() method.

  • This ensures proper resource management and prevents memory leaks caused by unclosed resources.

  1. 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 variable context.

    • 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 and getContext methods to set and get the context respectively, ensuring proper management of the context reference.

  1. 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 a ClickListener callback.

    • The Activity class registers a click listener for the button in its onCreate method.

    • However, the onDestroy method, where the click listener should be unregistered, is not implemented. This can lead to a memory leak because the Activity instance holds a reference to the anonymous inner class implementing the ClickListener interface.

    • When the Activity instance is no longer needed, it might not be garbage collected because it's still referenced by the Button 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.

  1. 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 a loadBitmap function that loads a bitmap from an input stream using BitmapFactory.decodeStream.

    • In the main function, we load a bitmap using the loadBitmap 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 method loadImage that uses Glide library to load an image from a URL into an ImageView.

  • 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.

  1. 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 extends TextView.

    • 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.

  1. 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 extends Fragment.

    • 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 uses FragmentTransaction 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:

  1. 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.

  2. Usage: Your program utilizes these objects as needed during its execution.

  3. 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.

  4. 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.

  5. 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, and obj3.

  • Initially, obj1 and obj2 reference different objects.

  • Later, we make obj1 reference the same object as obj2, effectively making the original object referenced by obj1 unreachable.

  • We then set obj2 to null, making the object it referenced unreachable as well.

  • At this point, the objects originally referenced by obj1 and obj2 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! 🌟

Check out my Instagram page

Check out my LinkedIn

0
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!