前言
内存泄漏即该被释放的内存没有被及时的释放, 一直被某个或某些实例所持有却不再使用导致 GC 不能回收.
文末准备了一份完整系统的进阶提升的技术大纲和学习资料, 希望对于有一定工作经验但是技术还需要提升的朋友提供一个方向参考, 以及免去不必要的网上到处搜资料时间精力.
Java 内存分配策略
Java 程序运行时的内存分配策略有三种, 分别是静态分配, 栈式分配, 和堆式分配. 对应的三种策略使用的内存空间是要分别是静态存储区(也称方法区), 栈区, 和堆区.
静态存储区(方法区): 主要存放静态数据, 全局 static 数据和常量. 这块内存在程序编译时就已经分配好, 并且在程序整个运行期间都存在.
栈区: 当方法执行时, 方法内部的局部变量都建立在栈内存中, 并在方法结束后自动释放分配的内存. 因为栈内存分配是在处理器的指令集当中所以效率很高, 但是分配的内存容量有限.
堆区: 又称动态内存分配, 通常就是指在程序运行时直接 new 出来的内存. 这部分内存在不适用时将会由 Java 垃圾回收器来负责回收.
栈与堆的区别:
在方法体内定义的 (局部变量) 一些基本类型的变量和对象的引用变量都在方法的栈内存中分配. 当在一段方法块中定义一个变量时, Java 就会在栈中为其分配内存, 当超出变量作用域时, 该变量也就无效了, 此时占用的内存就会释放, 然后会被重新利用.
堆内存用来存放所有 new 出来的对象 (包括该对象内的所有成员变量) 和数组. 在堆中分配的内存, 由 Java 垃圾回收管理器来自动管理. 在堆中创建一个对象或者数组, 可以在栈中定义一个特殊的变量, 这个变量的取值等于数组或对象在堆内存中的首地址, 这个特殊的变量就是我们上面提到的引用变量. 我们可以通过引用变量来访问堆内存中的对象或者数组.
举个例子:
- public class Sample {
- int s1 = 0;
- Sample mSample1 = new Sample();
- public void method() {
- int s2 = 0;
- Sample mSample2 = new Sample();
- }
- }
- Sample mSample3 = new Sample();
如上局部变量 s2 和 mSample2 存放在栈内存中, mSample3 所指向的对象存放在堆内存中, 包括该对象的成员变量 s1 和 mSample1 也存放在堆中, 而它自己则存放在栈中.
结论:
局部变量的基本类型和引用存储在栈内存中, 引用的实体存储在堆中.-- 因它们存在于方法中, 随方法的生命周期而结束.
成员变量全部存储于堆中(包括基本数据类型, 引用和引用的对象实体).-- 因为它们属于类, 类对象终究要被 new 出来使用.
了解了 Java 的内存分配之后, 我们再来看看 Java 是怎么管理内存.
Java 是如何管理内存
由程序分配内存, GC 来释放内存. 内存释放的原理为该对象或者数组不再被引用, 则 JVM 会在适当的时候回收内存.
内存管理算法:
引用计数法: 对象内部定义引用变量, 当该对象被某个引用变量引用时则计数加 1, 当对象的某个引用变量超出生命周期或者引用了新的变量时, 计数减 1. 任何引用计数为 0 的对象实例都可以被 GC. 这种算法的优点是: 引用计数收集器可以很快的执行, 交织在程序运行中. 对程序需要不被长时间打断的实时环境比较有利. 缺点: 无法检测出循环引用.
引用计数无法解决的循环引用问题如下:
- public void method() {
- //Sample count=1
- Sample ob1 = new Sample();
- //Sample count=2
- Sample ob2 = new Sample();
- //Sample count=3
- ob1.mSample = ob2;
- //Sample count=4
- ob2.mSample = ob1;
- //Sample count=3
- ob1=null;
- //Sample count=2
- ob2=null;
- // 计数为 2, 不能被 GC
- }
Java 可以作为 GC ROOT 的对象有: 虚拟机栈中引用的对象(本地变量表), 方法区中静态属性引用的对象, 方法区中常量引用的对象, 本地方法栈中引用的对象(Native 对象)
标记清除法: 从根节点集合进行扫描, 标记存活的对象, 然后再扫描整个空间, 对未标记的对象进行回收. 在存活对象较多的情况下, 效率很高, 但是会造成内存碎片.
标记整理算法: 同标记清除法, 只不过在回收对象时, 对存活的对象进行移动. 虽然解决了内存碎片的问题但是增加了内存的开销.
复制算法: 此方法为克服句柄的开销和解决堆碎片. 把堆分为一个对象面和多个空闲面. 把存活的对象 copy 到空闲面, 主要空闲面就变成了对象面, 原来的对象面就变成了空闲面. 这样增加了内存的开销, 且在交换过程中程序会暂停执行.
分代算法:
分代垃圾回收策略, 是基于: 不同的对象的生命周期是不一样的. 因此, 不同生命周期的对象可以采取不同的回收算法, 以便提高回收效率.
年轻代:
所有新生成的对象首先都是存放在年轻代. 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象.
新生代内存按照 8:1:1 的比例分为一个 eden 区和两个 survivor(survivor0,survivor1)区. 一个 Eden 区, 两个 Survivor 区(一般而言). 大部分对象在 Eden 区中生成. 回收时先将 eden 区存活对象复制到一个 survivor0 区, 然后清空 eden 区, 当这个 survivor0 区也存放满了时, 则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区, 然后清空 eden 和这个 survivor0 区, 此时 survivor0 区是空的, 然后将 survivor0 区和 survivor1 区交换, 即保持 survivor1 区为空, 如此往复.
当 survivor1 区不足以存放 eden 和 survivor0 的存活对象时, 就将存活对象直接存放到老年代. 若是老年代也满了就会触发一次 Full GC, 也就是新生代, 老年代都进行回收
新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden 区满了才触发)
年老代:
在年轻代中经历了 N 次垃圾回收后仍然存活的对象, 就会被放到年老代中. 因此, 可以认为年老代中存放的都是一些生命周期较长的对象.
内存比新生代也大很多(大概比例是 1:2), 当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低, 老年代对象存活时间比较长, 存活率标记高.
持久代:
用于存放静态文件, 如 Java 类, 方法等. 持久代对垃圾回收没有显著影响, 但是有些应用可能动态生成或者调用一些 class, 例如 Hibernate 等, 在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类.
Android 常见的内存泄漏汇总
集合类泄漏
先看一段代码
- List<Object> objectList = new ArrayList<>();
- for (int i = 0; i <10; i++) {
- Object o = new Object();
- objectList.add(o);
- o = null;
- }
上面的实例, 虽然在循环中把引用 o 释放了, 但是它被添加到了 objectList 中, 所以 objectList 也持有对象的引用, 此时该对象是无法被 GC 的. 因此对象如果添加到集合中, 还必须从中删除, 最简单的方法
- // 释放 objectList
- objectList.clear();
- objectList=null;
单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长, 所以如果使用不恰当的话, 很容易造成内存泄漏. 比如下面一个典型的例子.
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass(Context context) {
- this.mContext = context;
- }
- public SingleInstanceClass getInstance(Context context) {
- if (instance == null) {
- instance = new SingleInstanceClass(context);
- }
- return instance;
- }
- }
正如前面所说, 静态变量的生命周期等同于应用的生命周期, 此处传入的 Context 参数便是祸端. 如果传递进去的是 Activity 或者 Fragment, 由于单例一直持有它们的引用, 即便 Activity 或者 Fragment 销毁了, 也不会回收其内存. 特别是一些庞大的 Activity 非常容易导致 OOM.
正确的写法应该是传递 Application 的 Context, 因为 Application 的生命周期就是整个应用的生命周期, 所以没有任何的问题.
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass(Context context) {
- this.mContext = context.getApplicationContext();// 使用 Application 的 context
- }
- public SingleInstanceClass getInstance(Context context) {
- if (instance == null) {
- instance = new SingleInstanceClass(context);
- }
- return instance;
- }
- }
- or
- // 在 Application 中定义获取全局的 context 的方法
- /**
- * 获取全局的 context
- * @return 返回全局 context 对象
- */
- public static Context getContext(){
- return context;
- }
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass() {
- mContext=MyApplication.getContext;
- }
- public SingleInstanceClass getInstance() {
- if (instance == null) {
- instance = new SingleInstanceClass();
- }
- return instance;
- }
- }
匿名内部类 / 非静态内部类和异步线程
非静态内部类创建静态实例造成的内存泄漏
我们都知道非静态内部类是默认持有外部类的引用的, 如果在内部类中定义单例实例, 会导致外部类无法释放. 如下面代码:
- public class TestActivity extends AppCompatActivity {
- public static InnerClass innerClass = null;
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (innerClass == null)
- innerClass = new InnerClass();
- }
- private class InnerClass {
- //...
- }
- }
当 TestActivity 销毁时, 因为 innerClass 生命周期等同于应用生命周期, 但是它又持有 TestActivity 的引用, 因此导致内存泄漏.
正确做法应将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例, 如果需要使用 Context, 请按照上面推荐的使用 Application 的 Context. 当然, Application 的 context 不是万能的, 所以也不能随便乱用, 对于有些地方则必须使用 Activity 的 Context, 对于 Application,Service,Activity 三者的 Context 的应用场景如下:
匿名内部类
Android 开发经常会继承实现 Activity/Fragment/View, 此时如果你使用了匿名类, 并被异步线程持有了, 那要小心了, 如果没有任何措施这样一定会导致泄露. 如下代码:
- public class TestActivity extends AppCompatActivity {
- //....
- private Runnable runnable=new Runnable() {
- @Override
- public void run() {
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- //......
- }
- }
上面的 runnable 所引用的匿名内部类持有 TestActivity 的引用, 当将其传入异步线程中, 线程与 Activity 生命周期不一致就会导致内存泄漏.
Handler 造成的内存泄漏
Handler 造成内存泄漏的根本原因是因为, Handler 的生命周期与 Activity 或者 View 的生命周期不一致. Handler 属于 TLS(Thread Local Storage)生命周期同应用周期一样. 看下面的代码:
- public class TestActivity extends AppCompatActivity {
- private Handler mHandler = new Handler() {
- @Override
- public void dispatchMessage(Message msg) {
- super.dispatchMessage(msg);
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- //do your things
- }
- }, 60 * 1000 * 10);
- finish();
- }
- }
在该 TestActivity 中声明了一个延迟 10 分钟执行的消息 Message,mHandler 将其 push 进了消息队列 MessageQueue 里. 当该 Activity 被 finish()掉时, 延迟执行任务的 Message 还会继续存在于主线程中, 它持有该 Activity 的 Handler 引用, 所以此时 finish()掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类, 它会持有外部类的引用, 在这里就是指 TestActivity).
修复方法: 采用内部静态类以及弱引用方案. 代码如下:
- public class TestActivity extends AppCompatActivity {
- private MyHandler mHandler;
- private static class MyHandler extends Handler {
- private final WeakReference<TestActivity> mActivity;
- public MyHandler(TestActivity activity) {
- mActivity = new WeakReference<>(activity);
- }
- @Override
- public void dispatchMessage(Message msg) {
- super.dispatchMessage(msg);
- TestActivity activity = mActivity.get();
- //do your things
- }
- }
- private static final Runnable mRunnable = new Runnable() {
- @Override
- public void run() {
- //do your things
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mHandler = new MyHandler(this);
- mHandler.postAtTime(mRunnable, 1000 * 60 * 10);
- finish();
- }
- }
需要注意的是: 使用静态内部类 + WeakReference 这种方式, 每次使用前注意判空.
前面提到了 WeakReference, 所以这里就简单的说一下 Java 对象的几种引用类型.
Java 对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种.
ok, 继续回到主题. 前面所说的, 创建一个静态 Handler 内部类, 然后对 Handler 持有的对象使用弱引用, 这样在回收时也可以回收 Handler 持有的对象, 但是这样做虽然避免了 Activity 泄漏, 不过 Looper 线程的消息队列中还是可能会有待处理的消息, 所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息.
下面几个方法都可以移除 Message:
- public final void removeCallbacks(Runnable r);
- public final void removeCallbacks(Runnable r, Object token);
- public final void removeCallbacksAndMessages(Object token);
- public final void removeMessages(int what);
- public final void removeMessages(int what, Object object);
尽量避免使用 staic 成员变量
如果成员变量被声明为 static, 那我们都知道其生命周期将与整个 App 进程生命周期一样.
这会导致一系列问题, 如果你的 App 进程设计上是长驻内存的, 那即使 App 切到后台, 这部分内存也不会被释放. 按照现在手机 App 内存管理机制, 占内存较大的后台进程将优先回收, 意味着如果此 App 做过进程互保保活, 那会造成 App 在后台频繁重启. 就会出现一夜时间手机被消耗空了电量, 流量, 这样只会被用户弃用.
这里修复的方法是:
不要在类初始时初始化静态成员. 可以考虑 lazy 初始化.
架构设计上要思考是否真的有必要这样做, 尽量避免. 如果架构需要这么设计, 那么此对象的生命周期你有责任管理起来.
避免 override finalize():
finalize 方法被执行的时间不确定, 不能依赖与它来释放紧缺的资源. 时间不确定的原因是: 虚拟机调用 GC 的时间不确定以及 Finalize daemon 线程被调度到的时间不确定.
finalize 方法只会被执行一次, 即使对象被复活, 如果已经执行过了 finalize 方法, 再次被 GC 时也不会再执行了, 原因是: 含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该 Object 的, 而在 finalize 方法执行的时候, 该 object 所对应的 finalize Reference 会被释放掉, 即使在这个时候把该 object 复活(即用强引用引用住该 object ), 再第二次被 GC 的时候由于没有了 finalize reference 与之对应, 所以 finalize 方法不会再执行.
含有 Finalize 方法的 object 需要至少经过两轮 GC 才有可能被释放.
其它
内存泄漏检测工具强烈推荐 squareup 的 LeakCannary https://github.com/square/leakcanary , 但需要注意 Android 版本是 4.4 + 的, 否则会 Crash.
来源: https://juejin.im/post/5c73b10ee51d455f1c31132c