内存泄漏问题可以说是Android开发者最烦恼的问题之一了,项目中连续遇到几个内存泄漏问题,这篇文章主要说明下容易发生内存泄漏的场景以及检查分析内存泄漏的一些工具与方法。
在说内存泄露之前,需要先了解JVM的内存回收机制。
众所周知,Java是自带垃圾回收机制的,这使得Java程序员比C++程序员轻松许多,内存空间申请了,不用心心念念要加一句释放,Java虚拟机(JVM)会自行使用回收线程不定时地回收那些不再被需要的内存空间(注意回收的不是对象本身,而是对象占据的内存空间)。
Java没有指针,全凭引用来和对象进行关联,通过引用来操作对象。如果一个对象没有与任何引用关联,那么这个对象也就不太可能被使用到了,回收器便是把这些“无任何引用的垃圾对象”作为目标,回收了它们占据的内存空间。
JVM使用可达性分析法来分辨垃圾对象,这个方法设置了一系列的“GC Roots”对象作为索引起点,如果一个对象与起点对象之间均无可达路径,那么这个不可达的对象就会成为回收对象。这种方法处理两个对象相互引用的问题,如果两个对象均没有外部引用,会被判断为不可达对象进而被回收(如下图)。
而GC Roots特指的是垃圾收集器(Garbage Collector)的对象,一个对象可以属于多个 Root,GC Roots 有几下种:
Class | 由系统class loader加载的对象,这些类是不能够被回收的,它们可以以静态字段的方式持有其它对象。需要注意的是,自定义的class loader加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为 Roots,否则它们并不是 Roots |
Thread | 活跃线程中的引用对象 |
Stack Local | Java方法的本地变量或参数 |
JNI Local | JNI方法的local变量或参数 |
JNI Global | 全局JNI引用 |
Monitor Used | 用于同步的监控对象 |
Held by JVM | 用于JVM特殊目的而由GC保留的对象,但实际上这个与 JVM 的实现是有关的,可能已知的一些类型是系统类加载器、一些 JVM 熟悉的重要异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等,然而 JVM 并没有为这些对象提供其它的信息,因此就只有留给分析分员去确定哪些是属于 “JVM 持有” 的了 |
虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。这些对象积累在内存中,直到程序结束,就是我们所说的“内存泄漏”。
当然了,用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。
总结什么是内存泄露:当某些内存不受GC控制时,这些内存就发生内存泄漏了。
GC 过程是和对象引用的类型是严重相关的,我们在平时接触到的一般有三种引用类型,强引用、软引用、弱引用和虚引用:
级别 | 回收时机 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | |
软引用 | 在内存不足的时候 | 联合 ReferenceQueue 构造有效期短/占内存大/生命周期长的对象的二级高速缓冲器(内存不足时才清空) | 内存不足时终止 |
弱引用 | 在垃圾回收时 | 联合 ReferenceQueue 构造有效期短/占内存大/生命周期长的对象的一级高速缓冲器(系统发生GC则清空) | GC 运行后终止 |
虚引用 | 在垃圾回收时 | 联合 ReferenceQueue 来跟踪对象被垃圾回收器回收的活动 | GC 运行后终止 |
在 Java/Android 开发中,为了防止内存溢出,在处理一些占内存大而且生命周期比较长对象的时候,可以尽量应用软引用和弱引用,软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中,利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用程序的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
比如下面的这个代码:
public class DimenUtil {
private Context mContext;
private static DimenUtil instance;
public DimenUtil(Context context) {
this.mContext = context;
}
public static synchronized DimenUtil getInstance(Context context) {
if(instance == null) {
instance = new DimenUtil(context);
}
return instance;
}
public float dip2px(float dipValue) {
float scale = this.mContext.getResources().getDisplayMetrics().density;
return dipValue * scale + 0.5F;
}
}
这是一个尺寸转换的工具类,它是一个单例类,我们暂且不讨论这种代码实现是否优雅,如果我们在调用DimenUtil.getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。
以Activity为例,当我们启动一个Activity,并调用DimenUtil.getInstance(Context context)方法去获DimenUtil的单例,传入Activity.this作为context,这样DimenUtil类的单例instance就持有了Activity的引用,当我们退出Activity时,该Activity就没有用了,但是因为intance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。
为了避免这样单例导致内存泄露,我们可以将context参数改为全局的上下文:
public DimenUtil(Context context) {
this.mContext = context.getApplicationContext();
}
这样不管外部调用时传入的context参数是activity还是application时,都能保证不被context不会泄露。这种泄露是最常见的,也是最容易解决的一种内存泄露案例。
在Java中,内部类虽然和外部类都写在同一个Java文件中,但是编译完成后,还是会生成各自的class文件,之所以内部类对象可以访问外部类对象中的成员(包括成员变量和成员方法),是因为内部类对象持有指向外部类对象的引用,那为什么会持有这个引用?
比如下面的代码:
public class Outer {
int outerField = 0;
public class Inner{
void InnerMethod(){
int i = outerField;
}
}
}
在编译成class文件后,会生成Outer.class和Outer$Inner.class这两个class文件,通过反编译Outer$Inner.class会发现里面的代码大致是这样的:
class Outer$Inner{
final Outer this$0;
public Outer$Inner(Outer outer){
this.this$0 = outer;
super();
}
void InnerMethod(){
int i = this.this$0.outerField;
}
}
我们会发现:
这也就解释了内部类为什么能访问外部类的成员了。
看到这里我们能想到,由于非静态内部类(包括匿名内部类)默认就会持有外部类的引用,那么当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
在Android开发中这种情况出现得最多的场景就是使用Handler,很多开发者在使用Handler是这样写的:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// do something
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
notifySomething();
}
private void notifySomething() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
}
也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄露呢,显然不是这样的!
熟悉Handler消息机制的童鞋都知道,mHandler会作为成员变量保存在发送的消息msg中,代码如下:
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
上面在介绍GC的垃圾回收机制时有说到,强引用会造成GC回收不了,而弱引用不会,那我们在解决这种要使用内部类,但又要规避内存泄露时,一般都会采用静态内部类+弱引用的方式:
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
notifySomething();
}
private void notifySomething() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference mRef;
public MyHandler(MainActivity activity) {
mRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mRef.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}
但弱应用也不是万能的,比如下面的代码:
pulic class LocationManager {
private LocationClient mLocationClient;
private static LocationManager sInstance;
public LocationManager getInstance() {
if (sInstance == null) {
sInstance = new LocationManager();
}
}
private LocationManager() {
mLocationClient = new LocationClient();
}
public void startLocation(final LocationListener listener) {
mLocationClient.requestLocation(new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation location) {
if (location.getErrorCode() == 0) {
if (listener != null) {
listener.onLocationSuccess(location);
}
} else {
if (listener != null) {
listener.onLocationFailure(location.getErrorInfo());
}
}
}
});
}
}
上面的这个单例类的作用是封装了定位的业务逻辑,在需要使用当前位置时调用startLocation(),然后传入一个回调的listener参数来接收定位结果。比如像下面的代码这样使用:
public class MainActivity extends AppCompatActivity {
private TextView mCityNameTxt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCityNameTxt = findViewById(R.id.txt_city_name);
LocationManager.getInstance().startLocation(new LocationListener() {
@Override
public void onLocationSuccess(AMapLocation location) {
mCityNameTxt.setText("当前城市:" + location.getCityName());
}
@Override
public void onLocationFailure(AMapLocation location) {
mCityNameTxt.setText("获取当前城市失败");
}
});
}
}
通过上面对非静态内部类持有外部类引用的讲解,我们肯定仔细一看就会发现代码中的LocationListener的匿名内部类持有了外部类也就是MainActivity的引用,而这个匿名内部类又被作为参数listener传递到了LocationManager的startLocation方法内部,而listener又被mLocationClient内部的requestLocation方法内部给持有了,如果在定位请求结果还没回来时就退出MainActivity,毫无疑问MainActivity就会发生内存泄露了。
我们一看到这种场景,可能马上能想到的解决办法就是在LocationManager的startLocation方法内部对listener参数使用弱应用,像下面这样:
public void startLocation(final LocationListener listener) {
final WeakReference mRef = new WeakReference<>(listener);
mLocationClient.requestLocation(new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation location) {
if (location.getErrorCode() == 0) {
if (mRef.get() != null) {
mRef.get().onLocationSuccess(location);
}
} else {
if (mRef.get() != null) {
mRef.get().onLocationFailure(location.getErrorInfo());
}
}
}
});
}
这下listener就不会被强引用,也就不会内存泄露了吧!但问题真的有这么简单吗?
多运行几次代码会发现有时候等了很长时间,定位结果无论成功或者失败都不会回调,这肯定不正常啊!通过debug发现有时候当MainActivty还没有退出时,mRef.get() 就已经为null了,这是因为GC在回收内存时发现listener对象只有一个弱引用持有,会立即回收掉listener对象,而GC触发的时机又不定,这也就是为什么有时候发起定位请求后一直不回调定位结果的原因。
对于这种情况我能想到的唯一一种解决办法是在LocationManager内部使用一个全局变量来接受listener,再提供一个stopLocation方法供MainActivity在回调onDestory()方法时调用,用来停止定位和释放掉listener对象的引用:
public class LocationManager {
private LocationListener mListener;
......
public void stopLocation() {
if (mListener != null) {
mLocationClient.stopLocation();
mListener = null;
}
}
}
在项目开发过程中,我们或多或少都会使用Android提供的一些Service类,如ConnectivityManager、PowerManager、AlarmManager 、NotificationManager等,这些Service类使用起来很方便,都是通过Context.getSystemService方法来获得。但如果使用不注意,就会造成内存泄露,比如下面的代码:
待补充
待补充
在实际开发过程中难免会有把对象添加到集合容器(比如 ArrayList)中的需求,如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法当然就是在使用完之后将该对象从容器中移除。
但下面这种情况也会内存泄露:
Set set = new HashSet<>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set) {
System.out.println(person);
}
还有一种情况是因为频繁的内存分配和释放,导致内存区域里面存在很多碎片,当这些碎片足够多,new 一个大对象的时候,所有的碎片中没有一个碎片足够大以分配给这个对象,但是所有的碎片空间加起来又是足够的时候,就会出现 OOM,而且这种 OOM 从某种意义上讲,是完全能够避免的。
由于产生内存碎片的场景很多,从 Memory Monitor 来看,下面场景的内存抖动是很容易产生内存碎片的:
最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;或者是 String 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候)。
如果是其他的问题,就需要通过 Memory Monitor 去观察内存的实时分配释放情况,找到内存抖动的地方修复它,或者如果当出现下面这种情况下的 OOM 时,也是由于内存碎片导致无法分配内存:
待补充
待补充
待补充