本篇是作为内存泄漏入门,主要说的是一些关于内存泄漏的概念,包括什么是内存泄漏,内存分配的几种策略,为什么会造成内存泄漏 及 如何避免内存泄漏等。
对于一个APP的评测,最直接的评分点就是用户体验,用户体验除界面设计外,就数APP是否运行流畅较为重要,当APP中出现越来越多内存泄漏时,卡顿特效就会随之而来。类比下电脑,cpu性能低下或内存不足时,程序运行效率就会降低,常见的现象就是运行卡顿。或许你会说现在的安卓手机配置多牛逼,8核的骁龙cpu,4G的运行内存,流畅的运行一个app足够啦,但实际情况是这样的吗?一个安卓APP是运行在一个dalvik虚拟机上的,系统分配给一个dalvik虚拟机的内存是固定的,如:16M,32M,64M(不同手机分配的内存不一样,可能现在的国产机分配的内存会更大,但绝对不会分配全部内存给一个安卓APP),分配给一个APP的运行内存只有几十M,想想是不是有点少了呢?所以在这有限的运行内存中,想让一个APP一直流畅的运行,解决内存泄漏是十分必要的。
作为一个使用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就不会去回收它,这就产生了内存泄漏,下面开始进入正题。
当一个对象已经不需要再使用,本该被回收时,而有另一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。简而言之,内存不在GC掌控之内了。
1)静态的
静态的存储区:内存在程序编译的时候就已经分配好,这块内存在整个程序的运行期间都一直存在。它主要存放静态数据、全局的static数据和一些常量。
2)栈式的
在执行函数(方法)时,函数中的一些内部变量的存储都可以放在栈中创建,函数执行结束时,这些存储单元就会自动被释放。
3)堆式的
也叫动态内存分配。java中需要调用new来申请分配一块内存,依赖GC机制回收。而c/c++则可以通过调用malloc来申请分配一块内存,并且需要自己负责释放。c/c++是可以自己掌控内存的,但要求程序员有很高的素养来解决内存的问题。而java这块对程序员而言并没有很好的方法去解决垃圾内存,需要在编程时就注意自己良好的编程习惯。
下面通过一段代码,来说明一个类被创建时,往堆栈都存放了些什么:
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()实例在堆中
}
}
成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)——因为它们属于类,类的实例是存放在堆中的。
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储在堆中。——因为它们属于方法当中的变量,生命周期会随着方法一直结束。
单例模式导致对象无法释放从而造成内存泄露
/**
* @创建者 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也不会被回收销毁。
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
如果不在onCreate()中获取CommonUtil对象的话,在改变屏幕方向后,竖屏的MemoryLeakActivity在调用onDestroy()时,会被GC回收。而这里出现了内存泄漏,就是因为在代码中获取CommonUtil对象搞的鬼。详情如下图所示:
屏幕旋转
既然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实例了。
没有内存泄漏了
在上一篇《性能优化——内存泄漏(1)入门篇》中,介绍了内存泄漏的基本概念,并举了一个Demo,结合简单的代码分析,猜测出Demo中存在内存泄漏,并用Android Studio自带的Memory Monitor证明了我们的猜测,但开发中,业务逻辑可能比较复杂,对象引用繁多,难道都要这样去做代码分析吗?肯定不行,程序员的精力有限,且“很懒”(追求效率),我们需要工具来帮助我们进行分析。下面就来看看都有什么神器吧。
System Information是Android Studio自带的分析工具,可以通过它来判断APP整体是否存在内存泄漏。
一个不存在内存泄漏的APP,在其退出并执行过GC后,APP中所有的View和Activity都会被销毁,所以,我们可以根据退出后内存中View和Activity的数量来判断这个APP是否存在内存泄漏。操作如下:
以上一篇中的Demo为例:
运行APP-->打开一个Activity-->屏幕旋转-->点返回键(两次)直到到桌面-->执行GC-->System Information-->Memory Usage
通过上述操作,会生成一个txt文本,其中就记录着View和Activity的数量。如下图所示,它们的值都为0(主要看Activities),说明本APP不存在内存泄漏。相反的,只要值不为0,那么APP是存在内存泄漏的。
这种方式只能判断整个APP是否存在内存泄漏,但无法知道哪里有内存泄漏,也无法得知是哪些对象引用造成的内存泄漏。
Analyzer Tasks 是Android Studio自带的分析工具,可以帮助我们快速定位内存泄漏,而且使用上 very easy~
打开一个内存快照(.hprof文件),Android Studio的右侧会出现Analyzer Tasks。
打开Analyzer Tasks后,可以发现有2个选项和一个按钮,说明如下图所示,我们需要的就是第一个"Detect Leaked Activities"。
执行任务后,下方的Analysis Results中会得到泄漏的Activity,而且直接定位到了MemoryLeakActivity[0],要上篇中得到的结果一样。