Anonymous inner class/Lambda Java and Kotlin who will cause memory leaks?

Author: Little murloc loves programming

foreword

Memory leaks are an eternal topic in the programming world, especially for Android development. To make your App more elegant, it is imperative to understand and manage memory leaks.
Through this article, you will learn:

  1. What is a memory leak?
  2. Android common memory leak scenarios
  3. Can Java anonymous inner classes cause leaks?
  4. Do Java's Lambdas leak?
  5. Do Kotlin anonymous inner classes cause leaks?
  6. Do Kotlin Lambdas leak?
  7. Do Kotlin higher order functions leak?
  8. Summary of memory leaks

1. What is a memory leak?

Simple Memory Distribution

As shown in the figure above, when the system allocates memory, it will look for free memory blocks for allocation (some require continuous storage space).
If the allocation is successful, the memory block is marked as occupied, and when the memory block is no longer used, it is set as free.

Occupation and being occupied involve the allocation and release of memory, and have different packages in different programming languages.

C allocate/free memory functions:

Allocation: malloc function
release: free function

C++ allocate/free memory functions:

Allocation: new function
Release: delete function

C/C++ requires the programmer to manually allocate and free memory, and we know that manual things are easy to miss.

If a piece of memory is never used again, but has not been reclaimed, then this piece of memory can never be reused, which is a memory leak

Java memory leak

In view of the fact that C/C++ needs to manually release memory, which is easy to miss and eventually cause memory leaks, Java has improved the memory recovery mechanism: no programmers are required to manually release memory, and the JVM system has a GC mechanism that regularly scans objects that are no longer referenced
. Release the memory space occupied by the object.

You may be wondering: Since there are GC mechanisms, why are there leaks?
Because the GC judges whether the object is still in use based on reachability, when the GC action occurs, if an object is held by the gc root object, it cannot be recycled.

As shown in the figure above, obj1 and obj5 are directly or indirectly held by gc root, and they will not be recycled, while obj6 and obj10 are not held by gc root, and they can be recycled.

Common objects as gc root

When the JVM initiates a GC action, it needs to judge the accessibility of the object from the gc root. Common gc root objects:

The gc root involved in troubleshooting memory leaks in development is:

JNI variables, static references, active threads

If JNI development is not involved, we pay more attention to the latter two.

At this point, we know the cause of Java memory leaks:

Objects that are no longer used, because of some improper operations, they are held by gc root and cannot be recycled, and eventually memory leaks

2. Android common memory leak scenarios

classic leak problem

Improper use of Handler leaks

First look at the familiar Demo:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    };
}

There is an anonymous inner class above, which inherits from Handler.
We know that in Java, anonymous inner classes hold external class references by default, and the compiler will prompt here:

This Handler class should be static or leaks might occur (anonymous android.os.Handler)

meaning is:

It is recommended to use static classes to inherit Handler, because there may be a risk of memory leaks when using anonymous inner classes

Let's do an experiment, the operation steps: open the Activity, close the Activity, observe the memory usage and see if there is a memory leak.

Here comes the question: Will the above code have a memory leak?
The answer is of course no, because we are not using the handler object.

Modify the code and add the following code in onCreate:

        handler.sendEmptyMessageDelayed(2, 5000);

Will there be a memory leak at this point?
Of course, the naked eye cannot prove whether it is leaked. We use the performance analysis tool that comes with Android Studio: Profiler for analysis:

Sure enough, the Activity leaked.

How to avoid memory leaks in this scenario?

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new MyHandler().sendEmptyMessageDelayed(2, 5000);
    }

    static class MyHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    }
}

Use the static inner class to implement the Handler function. The static inner class does not hold the external class reference by default.
As a result of the test, there is no memory leak.

Whether it is an anonymous inner class or a static inner class, there is no explicit reference to the outer class. Since the anonymous inner class will leak, why do you need an anonymous inner class?
Advantages of anonymous inner classes:

  1. No need to redefine a new named class
  2. Qualified anonymous inner classes can be converted into Lambda expressions, concise
  3. Anonymous inner classes can directly access outer class references

If you need to pop up a Toast when you receive a message now.
The implementation for anonymous inner classes is simple:

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Toast.makeText(ThirdActivity.this, "hello world", Toast.LENGTH_SHORT).show();
        }
    };

Because it holds external class references by default.

For static inner classes, it prompts that the outer class object cannot be accessed.

It needs to pass the external class reference to it separately, which is more cumbersome than the anonymous inner class.

The essential cause of Handler leaks

For the current demo, the anonymous inner class implicitly holds the outer class reference, and we need to find out which gc root directly/indirectly holds the anonymous inner class.

It can be seen from the figure that the Activity is finally held by Thread.
A brief review of the source code process:

  1. When the Handler object is constructed, the Looper of the current thread is bound, and the MessageQueue reference is held in the Looper
  2. The Looper of the current thread is stored in ThreadLocal in Thread
  3. When the Handler sends a message, construct the Message object, and the Message object holds the Handler reference
  4. Message objects will be placed in MessageQueue
  5. It can be inferred that Thread will indirectly hold Handler, and Handler will hold external class references, and eventually Thread will indirectly hold external class references, resulting in a leak

Improper use of threads leaks

Look at the simple demo first:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

Q: Will the above code have a memory leak?
Answer: Of course not, because the thread is not open.

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}

Analyze and analyze again, will there be a memory leak?
Consistent with the previous Handler, the anonymous inner class will hold a reference to the outer class, and the anonymous inner class itself is held by the thread, so a leak will occur.

How to avoid memory leaks in this scenario?

There are two ways:
the first one: replace the anonymous inner class with a static inner class
This method is similar to Handler processing.

The second: use Lambda to replace the anonymous inner class

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new Thread(() -> {
            try {
                Thread.sleep(200000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}

Lambda expressions do not implicitly hold external classes, so there is no risk of memory leaks in this scenario.

Improperly registered memory leak

To simulate a simple download process, first define a download management class:

public class DownloadManager {
   private DownloadManager() {
   }
   static class Inner {
      private static final DownloadManager ins = new DownloadManager();
   }
   public static DownloadManager getIns() {
      return Inner.ins;
   }
   private HashMap<String, DownloadListener> map = new HashMap();
   //模拟注册
   public void download(DownloadListener listener, String path) {
      map.put(path, listener);
      new Thread(() -> {
         //模拟下载
         listener.onSuc();
      }).start();
   }
}

interface DownloadListener {
   void onSuc();
   void onFail();
}

The download path is passed in externally, and the external caller is notified after the download is successful:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        DownloadManager.getIns().download(new DownloadListener() {
            @Override
            public void onSuc() {
                //更新UI
            }
            @Override
            public void onFail() {
            }
        }, "hello test");
    }
}

Because the UI needs to be updated when the callback is downloaded, an anonymous inner class is chosen to receive the callback, and because the anonymous inner class is held by a static variable: DownloadManager.ins.
That is to say:

The static variable acts as the gc root, indirectly holds the anonymous inner class, and finally holds the Activity, resulting in a leak

How to avoid memory leaks in this scenario?

There are two ways:

  1. The static inner class holds a weak reference to Activity
  2. DownloadManager provides an anti-registration method. When the Activity is destroyed, the anti-registration removes the callback from the Map

3. Can anonymous inner classes in Java cause leaks?

Thread holds anonymous inner class object

Some pre-knowledge about memory leaks has been passed, and then we analyze whether there are leaks in anonymous inner classes, Lambda expressions, and high-order functions from the perspective of bytecode.
Look at the demo first:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("fish", "hello world");
            }
        }).start();
    }
}

Guess the leak happens when we enter the Activity and then exit it?
Some friends will say: Of course, the thread holds the anonymous inner class object, and the anonymous inner class object holds the outer class (Activity) reference.
In fact, the thread here does not perform time-consuming tasks, and it ends soon. When the system recycles the Activity object, the thread has already ended, and it will no longer hold the anonymous inner class object.

How to determine that the anonymous inner class holds the outer class reference?
A very intuitive performance:

Accessing the instance variables of the outer class in the anonymous inner class, if the compiler does not prompt an error, it can be considered that the anonymous inner class holds the outer class reference

Of course, if you want to see the stone hammer, you have to start from the bytecode.

Java anonymous inner class Class file

Build it and find the product of Javac: in the directory starting with /build/intermediates/javac

You cannot see the anonymous inner class here, you need to find it in the file browser.

It can be seen that we only declared a ThirdActivity class, but generated two Class files, one of which is generated by an anonymous inner class, usually named as: outer class name + "$" + "number of inner classes" + ". class".
Drag it into Studio to view the content:

Obviously, there is the type of the outer class in the formal parameter of the anonymous inner class constructor, which will be passed in and assigned to the member variable of the anonymous inner class when the anonymous inner class is constructed.

Java anonymous inner class bytecode

There are many ways to view the bytecode, you can use the javap command:

javap -c ThirdActivity$1.class

You can also download the bytecode plugin in Android Studio:

Right-click on the source file and select View Bytecode:

As can be seen:

  1. The New instruction creates an anonymous inner class object and copies it to the top of the operand stack
  2. Load the external class object onto the top of the operand stack
  3. Call the anonymous inner class constructor and pass in the top object of the second step

In this way, an anonymous inner class is created and holds a reference to the outer class.

Back to the original question, will Java anonymous inner classes leak?

When the outer class is destroyed, if the anonymous inner class is held by the gc root (indirect/direct), then a memory leak will occur

4. Do Java's Lambdas leak?

Threads hold Lambda objects

Transform the anonymous inner class summarized above into Lambda (note: not all anonymous inner classes can be converted into Lambda expressions)

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(() -> {
            Log.d("fish", "hello world");
            Log.d("fish", "hello world2");
        }).start();
    }
}

Class file generated by Java Lambda

Java Lambda does not generate Class files.

Java Lambda bytecode

Java Lambda does not generate a Class file, but dynamically generates a Runnable object through the INVOKEDYNAMIC instruction, and finally passes it into the Thread.
It can be seen that the Lambda generated at this time does not hold external class references.

Java Lambda explicitly holds outer class reference

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(() -> {
            //显式持有外部类引用
            Log.d("fish", ThirdActivity.class.getName());
        }).start();
    }
}

Look at the bytecode again:

It can be seen that an external class reference is passed in.
Back to the original question, will Java Lambda leak?

  1. Lambda does not implicitly hold outer class references,
  2. If the external class reference is explicitly held in the Lambda, then similar to the Java anonymous internal class at this time, when the external class is destroyed, if the Lambda is held by the gc root (indirectly/directly), then a memory leak will occur

5. Can Kotlin anonymous inner classes cause leaks?

Thread holds anonymous inner class object

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread(object : Runnable {
            override fun run() {
                println("hello world")
            }
        }).start()
    }
}

Will the anonymous inner class hold the outer class reference at this point?
Start with the generated Class file.

Class file generated by Kotlin anonymous inner class

The Class directory generated by Kotlin compilation: build/tmp/kotlin-classes/ Find the generated Class file:

We found that the Class file was generated, naming rules: external class name + method name + the number of anonymous inner classes + ".class"

Kotlin anonymous inner class bytecode

It can be seen that no external class references are held.

Kotlin anonymous inner class explicitly holds outer class reference

class FourActivity : AppCompatActivity() {
    val name = "fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread(object : Runnable {
            override fun run() {
                println("hello world $name")
            }
        }).start()
    }
}

View the bytecode:

It can be seen that the constructor carries an external class reference.

Back to the original question, do Kotlin anonymous inner classes leak?

  1. Kotlin anonymous inner classes do not implicitly hold outer class references,
  2. If the external class reference is explicitly held in the Kotlin anonymous inner class, then similar to the Java anonymous inner class, when the outer class is destroyed, if the Lambda is held by the gc root (indirectly/directly), then it will happen memory leak

6. Do Kotlin Lambdas leak?

Threads hold Lambda objects

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread { println("hello world ") }
    }
}

Will the Lambda hold the outer class reference at this point?
Start with the generated Class file.

Class file generated by Kotlin Lambda

Kotlin Lambda does not generate Class files.

Kotlin Lambda bytecode

As can be seen, no external class references are held implicitly.

Kotlin Lambda explicitly holds external class reference

class FourActivity : AppCompatActivity() {
    val name = "fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread { println("hello world $name") }
    }
}

View the bytecode:

It can be seen that the constructor carries an external class reference.

Back to the original question, will Kotlin Lambda leak?

Consistent with Java Lambda expression

7. Do Kotlin higher-order functions leak?

What are higher order functions?

A function that takes a function type as a parameter or return value is called a higher-order function.
Higher-order functions are ubiquitous in Kotlin and are a great tool for Kotlin's concise writing.

Class files generated by higher-order functions

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        test {
            println("$it")
        }
    }
    //高阶函数作为形参
    private fun test(block:(String) -> Unit) {
        block.invoke("fish")
    }
}

A very simple high-order function, view the generated Class file:

View Kotlin Bytecode content:

final class com/fish/perform/FourActivity$onCreate$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function1 {

Inherited from Lambda, and implements the Function1 interface.
Its constructor has no formal parameters, indicating that no external class references will be passed in.

Bytecode for higher-order functions

The difference from the anonymous inner class and Lambda analyzed before (although higher-order functions can also be simplified by Lambda): the GETSTATIC instruction is involved.
This instruction means to obtain a reference to a higher-order function from a static variable, and the static variable has been initialized when the bytecode of the higher-order function is loaded:


It can be understood as follows:

  1. When the Class of the higher-order function is loaded, the instance is initialized and stored in a static variable
  2. When the higher-order function is called externally, obtain the higher-order function instance from the static variable

Higher-order functions explicitly hold outer class references

class FourActivity : AppCompatActivity() {
    val name="fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        test {
            println("$it:$name")
        }
    }
    //高阶函数作为形参
    private fun test(block:(String) -> Unit) {
        block.invoke("fish")
    }
}

View the bytecode:

The constructor holds an external class reference, and no static variable is generated at this time (it is not necessary to generate, if it is generated, it will be a proper memory leak)

Back to the original question, do higher-order functions leak?

  1. Higher-order functions do not implicitly hold outer class references
  2. If the external class reference is explicitly held in the higher-order function, then it is similar to the Java anonymous inner class. When the outer class is destroyed, if the higher-order function is held by the gc root (indirectly/directly), then it will A memory leak occurs

8. Memory leak summary

Simple understanding of memory leaks:

  1. Objects with a long life cycle hold objects with a short life cycle, resulting in objects with a short life cycle not being recycled in time after the end of the life cycle, resulting in memory that cannot be reused and eventually leaks
  2. Properly release references to short-lived objects

In order to help everyone better grasp the performance optimization in a comprehensive and clear manner, we have prepared relevant core notes (returning to the underlying logic):https://qr18.cn/FVlo89

Performance optimization core notes:https://qr18.cn/FVlo89

Startup optimization

Memory optimization

UI

optimization Network optimization

Bitmap optimization and image compression optimization : Multi-thread concurrency optimization and data transmission efficiency optimization Volume package optimizationhttps://qr18.cn/FVlo89




"Android Performance Monitoring Framework":https://qr18.cn/FVlo89

"Android Framework Study Manual":https://qr18.cn/AQpN4J

  1. Boot Init process
  2. Start the Zygote process at boot
  3. Start the SystemServer process at boot
  4. Binder driver
  5. AMS startup process
  6. The startup process of the PMS
  7. Launcher's startup process
  8. The four major components of Android
  9. Android system service - distribution process of Input event
  10. Android underlying rendering-screen refresh mechanism source code analysis
  11. Android source code analysis in practice

Guess you like

Origin blog.csdn.net/weixin_61845324/article/details/131786085