性能优化——内存泄漏(1)入门篇

96
CSDN_LQR
2017.06.29 14:57* 字数 2292

内存泄漏系列文章:
性能优化——内存泄漏(1)入门篇
性能优化——内存泄漏(2)工具分析篇
性能优化——内存泄漏(3)代码分析篇

一、简述

本篇是作为内存泄漏入门,主要说的是一些关于内存泄漏的概念,包括什么是内存泄漏,内存分配的几种策略,为什么会造成内存泄漏 及 如何避免内存泄漏等。

1、避免内存泄露的重要性

对于一个APP的评测,最直接的评分点就是用户体验,用户体验除界面设计外,就数APP是否运行流畅较为重要,当APP中出现越来越多内存泄漏时,卡顿特效就会随之而来。类比下电脑,cpu性能低下或内存不足时,程序运行效率就会降低,常见的现象就是运行卡顿。或许你会说现在的安卓手机配置多牛逼,8核的骁龙cpu,4G的运行内存,流畅的运行一个app足够啦,但实际情况是这样的吗?一个安卓APP是运行在一个dalvik虚拟机上的,系统分配给一个dalvik虚拟机的内存是固定的,如:16M,32M,64M(不同手机分配的内存不一样,可能现在的国产机分配的内存会更大,但绝对不会分配全部内存给一个安卓APP),分配给一个APP的运行内存只有几十M,想想是不是有点少了呢?所以在这有限的运行内存中,想让一个APP一直流畅的运行,解决内存泄漏是十分必要的。

2、java与c/c++的对比

作为一个使用java开发的程序员,我们知道,java比c/c++更“高级”,这里的“高级”不是说java比其他语言好(我不想引起圣战哈~),而是说java在内存申请与回收方面不需要人为管理,而c/c++则需要自己去分配内存和释放内存。下面对比下两者之间的差别:

  1. 申请内存:
    java只要在代码中new一个Object,系统就会自己计算并分配好内存大小;而c/c++则相对麻烦,需要调用malloc(size_t size),手动计算并传入要分配的内存值。

  2. 释放内存:
    java有回收机制,即GC,不需要调用(也可以通过代码调用),一段时间后便会自己去回收已经不需要的内存;而c/c++则需要手动调用free(void *ptr)来释放指针指向的内存空间。

所以说java比c/c++更“高级”,但是java的垃圾回收机制也没有那么智能,因为它在执行垃圾回收时需要根据一个标准去判断这块内存是否是垃圾,当这块垃圾不符合作为垃圾的标准时,GC就不会去回收它,这就产生了内存泄漏,下面开始进入正题。

  • 上述的标准是:某对象不再有任何的引用时才会进行回收。
  • 这里的内存指的是堆内存,堆中存放的就是引用指向的对象实体。

二、基本概念

1、什么是内存泄露

当一个对象已经不需要再使用,本该被回收时,而有另一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。简而言之,内存不在GC掌控之内了。

2、java中内存分配的几种策略

1)静态的

静态的存储区:内存在程序编译的时候就已经分配好,这块内存在整个程序的运行期间都一直存在。它主要存放静态数据、全局的static数据和一些常量。

2)栈式的

在执行函数(方法)时,函数中的一些内部变量的存储都可以放在栈中创建,函数执行结束时,这些存储单元就会自动被释放。

3)堆式的

也叫动态内存分配。java中需要调用new来申请分配一块内存,依赖GC机制回收。而c/c++则可以通过调用malloc来申请分配一块内存,并且需要自己负责释放。c/c++是可以自己掌控内存的,但要求程序员有很高的素养来解决内存的问题。而java这块对程序员而言并没有很好的方法去解决垃圾内存,需要在编程时就注意自己良好的编程习惯。

  • 堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致程序效率低下。
  • 对于栈,采用先进后出,完全不会产生碎片,运行效率高且稳定。

下面通过一段代码,来说明一个类被创建时,往堆栈都存放了些什么:

public class Main {
    int a = 1; // a变量在堆中
    Person pa = new Person(); // pa变量在堆中,new Person()实例也在堆中

    public void hehe() {
        int b = 1; // b变量在栈中
        Person pb = new Person(); // pb变量在栈中,但new Person()实例在堆中
    }
}
  • 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)——因为它们属于类,类的实例是存放在堆中的。
  • 局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储在堆中。——因为它们属于方法当中的变量,生命周期会随着方法一直结束。

3、java中一些特殊类

类型 回收时机 使用 生命周期
StrongReference 强引用 从不回收 对象的一般保存 JVM停止是才会终止
SoftReference 软引用 当内存不足时 SoftReference<T>结合ReferenceQueue,有效期短 内存不足时终止
WeakReference 弱引用 在垃圾回收时 同软件引用 GC后终止
PhatomReference 虚引用 在垃圾回收时 结合ReferenceQueue来跟踪对象被垃圾回收期回收的活动 GC后终止

开发时,为了防止内存溢出,处理一些比较占用内存并且生命周期长的对象时,可以尽量使用软引用和弱引用。

三、实例

1、内存泄露例子

单例模式导致对象无法释放从而造成内存泄露

/**
 * @创建者 CSDN_LQR
 * @描述 一个简单的单例
 */
public class CommonUtil {
    private static CommonUtil mInstance;
    private Context mContext;
    public CommonUtil(Context context) {
        mContext = context;
    }
    public static CommonUtil getmInstance(Context context) {
        if (mInstance == null) {
            synchronized (CommonUtil.class) {
                if (mInstance == null) {
                    mInstance = new CommonUtil(context);
                }
            }
        }
        return mInstance;
    }
    ...
}

这种单例工具类在开发中是很常见的,它本身并没有什么问题。但如果使用不善,那问题就来了:

/**
 * @创建者 CSDN_LQR
 * @描述 内存泄漏
 */
public class MemoryLeakActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        CommonUtil.getmInstance(this);
    }
}

在MemoryLeakActivity中获取CommonUtil对象时,把自己作为参数传给了CommonUtil,这会有什么问题呢?因为CommonUtil对象使用了static修饰,是静态变量,在整个APP的运行期内,GC不会回收CommonUtil实例,并且它持有了传入的Activity,当Activity调用onDestroy()销毁时(例如屏幕旋转时,Activity会重建),发现自己还被其他变量引用了,所以该Activity也不会被回收销毁。

2、Memory Monitor的简单使用

Android Studio提供了一套Monitors工具,可以实时查看APP的内存分配、CPU占用及网络等情况,本篇主要针对内存分配,所以使用Memory Monitor来验证上面的说法。

1)找到Memory Monitor

图中的几个说明很详细,请细看。


MemoryMonitors

2)运行APP,证明内存泄漏

先打开APP,看到目前分配的内存为3.43MB。


程序默认占用内存

接着打开MemoryLeakActivity界面(从这里开始),查看到APP目前分配的内存为3.51MB。


打开界面后查看内存占用

我旋转下屏幕,可以看到APP目前分配的内存增加到了3.60MB。(可以认为每创建一个简单的Activity就会占用大约0.1MB内存)


旋转屏幕后,查看内存占用

点击Initiate GC(启动GC),再点击Dump Java Heap(获取当前内存快照)。


启动GC并获取当前内存快照

在Capture区找到刚刚获取的内存快照,找到MemoryLeakActivity,可以发现内存中有2个实例。
其实上一步中点击Initiate去启动GC,只是证明竖屏时创建的MemoryLeakActivity已经没办法被GC回收,也就是MemoryLeakActivity[0]不在GC的掌握之内,即内存泄漏了。


内存快照

分别点击MemoryLeakActivity实例0和1,可以看到坚屏MemoryLeakActivity[0]还被CommonUtil引用,而横屏MemoryLeakActivity[1]没有被CommonUtil引用。


坚屏MemoryLeakActivity

横屏MemoryLeakActivity

3、为什么会内存泄漏

如果不在onCreate()中获取CommonUtil对象的话,在改变屏幕方向后,竖屏的MemoryLeakActivity在调用onDestroy()时,会被GC回收。而这里出现了内存泄漏,就是因为在代码中获取CommonUtil对象搞的鬼。详情如下图所示:


屏幕旋转

4、解决方案

既然CommonUtil实例是静态的,存在于整个APP生命周期中,而ApplicationContext在整个APP的生命周期中也一直存在,那就给它传ApplicationContext对象即可。代码修改如下:

public class MemoryLeakActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        CommonUtil.getmInstance(getApplicationContext());
    }
}

之后重覆上述步骤,可以看到,内存中只有一个MemoryLeakActivity实例了。


没有内存泄漏了
Android性能优化