欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android内存泄漏问题

程序员文章站 2022-07-15 14:38:41
...

Java内存垃圾回收由专门的垃圾回收(Garbage Collector,GC)后台线程维护,自动回收机制减轻了开发者的负担,让开发者能够更加专注于业务功能的处理。GC回收已经相对比较智能,能够辨别出简单的垃圾对象和正常使用的对象,但是对于功能逻辑上的垃圾对象还是无能为力的,要想理解内存泄漏产生的原因需要对Java内存回收机制有一定的了解。

在早期的系统中想要判断对象是不是垃圾对象通常使用数字引用的方式,有别的对象引用它,引用计数就加一,如果该对象的数字引用值大于0表示有对象还在使用它,如果引用计数为零那么系统中就没有任何对象再引用它,这样的对象就被认为是垃圾对象,可以被回收。引用计数存在的问题是当对象之间相互引用,如果相互引用的对象同时不再被需要,但它们的引用计数都还是大于零的。Java垃圾回收机制就没有再采用引用计数而是基于GCRoot来判定对象是否被引用,Java里的对象往往需要依赖别的对象提供的方法来完成任务,这种依赖关系就构成了一个有向依赖图,处于有向依赖图中的对象都是运行程序必不可少的对象,在有向依赖图外面的对象就属于垃圾对象可以被回收。
Android内存泄漏问题
通常作为GCRoot的对象包括如下的这些对象:Java运行时栈局部变量,Native方法栈引用的对象,静态变量,活动线程,等待中的锁对象等。如果在运行期间某些对象已经不再被应用需要,但是GCRoot里的对象却依然保持着它们的引用,GC线程在做垃圾回收的时候就无法回收它们。逻辑垃圾对象由于开发者错误的引用导致它们存活时间超出了真正需要存活的时间,也就是说长生命周期的对象持有了短生命周期的对象,这就是内存泄漏问题的本质。
Android内存泄漏问题
上图中绘制出了JVM虚拟机在做垃圾对象识别时使用的GCRoot引用查找原理,从图上的GCRoot无法查找到objC和objD的引用,因此它们即使引用计数都不为零,但通过GCRoot判定时不可达对象,在后面的垃圾回收就会被直接释放掉占用的内存。还需要注意的时前面一直在说活动线程,线程必须处在启动之后结束之前,这段时间内线程引用的对象才是可达的,如果线程对象还未启动或者已经退出运行状态,它内部引用的各种对象都不是可达的。

活动线程

在探讨活动线程导致的内存泄漏之前,需要先了解Java中静态内部类和普通内部类的区别,首先定义一个Hello的外部类,内部包含static类型的World类,还有一个普通的InnerWorld类,它的定义代码如下,使用javac将源代码编译成.class文件。

// 普通内部类和静态内部类
public class Hello {
	static class World { // 静态内部类
		void world() {
			System.out.println("Static World");
		}
}

class InnerWorld { // 普通内部类
	void inner() {
		System.out.println("Inner World");
	}
}

InnerWorld world = new InnerWorld(); // 普通成员属性没有问题,有this可以绑定
// static InnerWorld world2 = new InnerWorld(); 静态成员变量不行,编译器直接报错

public static void main(String[] args) {
	Hello hello = new Hello();
	Hello.InnerWorld world = hello.new InnerWorld(); // 需要依赖外部类的对象才能创建
	Hello.World world2 = new Hello.World(); // 不需要外部类对象,和普通类创建一样
}

代码中外部类Hello内部定义两个内部类,World类时静态内部类,InnerWorld是普通内部类,在main()方法测试时创建World类型的变量直接new即可,而创建InnerWorld类型的变量必须依附在外部Hello类型的变量上,否则编译器报错。编译后查看生成的.class文件有三个,其中Hello.class就是最外部的Hello类,Hello$World代表的是普通的内部类,Hello$InnerWorld是静态内部类InnerWorld生成的类。JDK中提供了javap工具可以查看.class文件的内部结构,将生成的两个内部类文件反编译。

// 普通内部类和静态内部类反编译代码
javap Hello$World
Compiled from "Hello.java"
class Hello$World { // 静态内部类和普通类没什么区别
	Hello$World();
	void world();
}

javap Hello$InnerWorld
Compiled from "Hello.java"
class Hello$InnerWorld { // 普通内部类需要绑定外部对象
	final Hello this$0; // 外部类对象
	Hello$InnerWorld(Hello); // 构造方法的参数就是外部对象
	void inner();
}

可以看到静态内部类和普通的类没有多大区别,唯一不同的是类名前面多了外包类Hello$,在看普通内部类它包含了一个Hello类型的成员属性,该属性在构造函数中被传递进来。这也就是为什么创建普通内部类的对象一定要用外部类对象.new操作符或者只能在外部类里面创建普通内部类对象。在Android应用开发中通常会使用new回调接口生成回调对象,其实编译器会生成匿名内部类,再在new的位置创建匿名内部类的实例,该回调对象内部就会包含外部对象的强引用。

现在定义简单的网络请求工具类HTTPNetUtils,工具类中有回调接口Callback,如果网络请求成功就回调onSuccess()方法,失败就会掉onFailure()方法,因为Android禁止在主线程发起网络请求,工具类中会使用子线程加载网络数据并且回调,在回调方法中将展示Toast投递到主线程队列中。

// 网络请求内存泄漏示例
public class HttpNetUtils {
	public interface Callback {
		void onSuccess(String text);
		void onFailure();
	}

	public static void get(final String url, final Callback callback) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				ThreadUtils.sleep(20000); // 模拟网络比较慢
				HttpURLConnection http = 
(HttpURLConnection) new URL(url).openConnection();
				// 省略HttpURLConnection请求配置
				int statusCode = http.getResponseCode();
				if (statusCode == 200) {
					callback.onSuccess(http.getResponseMessage());
				} else {
					callback.onFailure();
				}
				http.disconnect(); 
			}
		}).start();
	}
}

public class NetworkActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_network);
		// 匿名内部类创建的Callback对象会包含NetworkActivity.this对象
		HttpNetUtils.get("http://www.baidu.com", new HttpNetUtils.Callback() {
			@Override
			public void onSuccess(final String text) {
				showSuccess();
			}
			@Override
			public void onFailure() { }
		});
		 // 能够解决内存泄漏的回调
		// HttpNetUtils.get("http://www.baidu.com", new MyCallback(this)); 
	}
}

代码逻辑非常简单现在直接进入NetworkActivity界面之后立即退出并返回MainActivity界面,在Android Studio的Android Profiler工具窗口的Memory内存界面,不断点击最前面垃圾箱图标的GC按钮,点击后很有可能会出发JVM的垃圾回收操作,为了确保一定执行GC操作可以多点几次,然后点击带向下箭头图标的Dump Heap按钮导出当前JVM的堆信息。
Android内存泄漏问题
Android Profiler Memory内存分析
在老版本的Android Studio上可能还无法查看详细的对象引用信息,还需要把前面导出来的Heap文件转换格式放到MAT(Memory Analysis Tools)工具中查看,使用的3.1.2版本集成的Heap分析工具已经很强大,直接查看分析出来的GCRoot引用。
Android内存泄漏问题
从上图可以看到启动网络请求的线程本身处于启动之后结束之前是个活动线程,虽然Java方法栈并没有引用它,JVM内部会有ThreadGroup专门管理它的引用,活动线程由于引用了Callback回调匿名对象,而Callback匿名对象又引用了NetworkActivity最终导致NetworkActivity虽然已经退出但依然保持在内存中,在Thread线程没有结束之前始终不会被垃圾回收。在Android开发中通常都会使用线程池对象来执行网络请求,线程池很大可能会使用核心工作线程执行用户的请求,而核心线程一直存在也就会导致Activity一直不会被回收。

对这种匿名内部类回调对象导致的内存泄漏应该如何修改呢,Java中的对象引用分成了四种:强引用、软引用(SoftReference)、弱引用(WeakReference)和幻象引用(PhatomReference),普通的对象赋值操作就是强引用,对象外面包裹在SoftReference和WeakReference中就属于软引用、弱引用,幻象引用比较少用;强引用通常不会被GC回收,在对象没有强引用的情况下软引用会在内存不足时会被回收,弱引用在下一次GC时就会被回收。现在只要用静态内部类实现定义回调对象类,在静态回调类中包含外部对象的WeakReference弱引用,回调判断弱引用内部如果没有对象表明外部对象已经被垃圾回收就不必在做回调操作。

private static class MyCallback implements HttpNetUtils.Callback {
	private WeakReference<Activity> mActivity;
	public MyCallback(Activity activity) {
		this.mActivity = new WeakReference<>(activity);
	}
	@Override
	public void onSuccess(final String text) {
		final Activity activity = mActivity.get();
		if (activity != null && !activity.isFinishing()) {
			showSuccess() ;
		}
	}
	@Override
	public void onFailure() {
	}
}

代码1-41中实现Callback接口创建了静态内部类,由于静态内部类不保存外部对象的this引用,需要将外部对象从构造函数传递进来,不过静态内部类使用了WeakReference引用,只要Activity被销毁外部就不会在存在Activity的强引用,此时JVM内部执行GC操作就可以回收Activity对象,当缓慢的网络请求返回时检查外部引用已经为空就不再执行回调。

Java方法栈

Java在调用方法时会为每个方法创建栈帧并将它们加入到方法栈中,很显然当前方法栈中引用的对象都是程序运行过程中必定会引用到的对象。不过有些在方法栈中对象的生命周期比较长,比如Android应用的消息队列,它在Java 的main()方法的栈帧中,只要主线程消息循环不退出,它就始终存在,开发中经常会需要将某些更新UI的任务投递主线程队列中执行,如果在Activity退出那些任务还没有完成就会依然保持在消息队列中,而UI更新任务通常都会创建匿名的Runnable普通内部对象,普通内部对象都会包含外部对象的this引用。

在下面的HandlerActivity.onCreate()里用mHandler投递mRunnable对象到主线程的消息队列中,5秒钟之后才会执行,但在回调之前用户退出了HandlerActivity。
Handler导致内存泄漏示例

public class HandlerActivity extends AppCompatActivity {
	private Handler mHandler;
	private Runnable mRunnable;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_handler);
		mHandler = new Handler(Looper.getMainLooper());
		mRunnable = new Runnable() { // 普通匿名内部类引用Activity
				@Override
				public void run() {
					Toast.makeText(getApplicationContext(), "Hello world!",
            					Toast.LENGTH_SHORT).show();
				}
		};
		mHandler.postDelayed(mRunnable, 5000);
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		// mHandler.removeCallbacks(mRunnable); 需要在onDestroy的时候移除主线程回调
	}
}

下面的图展示了ActivityThread $H类内部引用了MessageQueue消息队列,消息队列又引用了Message对象,Message.mCallback正是mRunnable对象,mRunnable是匿名内部类对象会包含HandlerActivity引用导致该Activity的内存泄漏。对这种情况可以用前面的静态内部类加WeakReference来处理内存泄漏,也可以在onDestroy()周期回调中及时移除主线程回调对象。
Android内存泄漏问题
Java方法栈上MessageQueue导致的内存泄漏

静态变量

静态变量和Class对象的生命周期一直,也是一种长生命周期的对象,在GC操作可达性分析中静态变量引用的对象也被认作活动对象,不会被GC回收。单例对象通常被保存在静态成员变量里,它引用的对象也可能会出现内存泄漏,特别是有些单例对象会包含回调接口,注册之后的对象就会被单例对象引用。下面代码中用户账号管理类能够实现登录接口,有些界面在用户登录的情况下需要作出改变,可以使用观察者模式实现。

public class UserManager {
	public interface UserLoginListener {
		void onLogin();
	}
	private List<UserLoginListener> mUserListeners = new ArrayList<>();
	private static class UserManagerHolder {
		private static final UserManager INSTANCE = new UserManager();
	}

	public static UserManager getInstance() {
		return UserManagerHolder.INSTANCE;
	}

	public void registerListener(UserLoginListener listener) {
		mUserListeners.add(listener);
	}

	public void unregisterListener(UserLoginListener listener) {
		mUserListeners.remove(listener);
	}
}

public class SingletonActivity extends AppCompatActivity 
	implements UserManager.UserLoginListener {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_singleton);
		// Activity作为观察者注册到单例对象中
		UserManager.getInstance().registerListener(this);
	}

	@Override
	public void onLogin() {
		// do something
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
       // 需要注销监听,否则会导致内存泄漏
		// UserManager.getInstance().unregisterListener(this);
	}
}

SingletonActivity就需要观察用户的登录状态,它在onCreate()方法中将自己注册为观察者,当用户登录的时候就会通知到SingletonActivity。假如用户直接退出SingletonActivity由于UserManager单例对象依然引用了它的实例就会导致内存泄漏,需要在onDestroy()时注销监听。下图展示了直接退出没有注销用户登录观察者导致的内存泄漏引用链。
Android内存泄漏问题

集合内存泄漏

接着要讨论本人在Effective Java上看到的一个自定义栈的示例 ,栈结构可以用链表也可以用数组来实现,通常还要支持push和pop操作,现在给出简单的数组实现代码。

public class MyStack {
	private int mTop;
	private Object[] mData;
	public void push(Object obj) {
         // 索引检查
		mData[mTop++] = obj;
		mSize++;
	}
	public Object pop() { // 需要先执行top指针检查
		Object tmp = mData[--mTop];
		// mData[mTop] = null; 需要注意置空否则会引起内存泄漏
		mSize--;
		return tmp;
	}
}

上面的代码中没有将退栈的对象置空,假如用户有下面的调用序列,先push三个对象之后再pop两个对象,在栈数组mData中引用了三个对象,而用户真正需要的对象只有最底下的那个对象,上面的两个对象在逻辑上已经是垃圾对象,由于栈数组依然保持着它的引用导致垃圾对象无法被回收。在退栈操作之后如果及时把栈顶元素的引用置空就不会出现内存泄漏问题了,这也就是为什么Java中对象确定不再使用后要把它置空,让GC线程能及时将对象回收。

myStack.push(new Object()); // 放入对象
myStack.push(new Object());
myStack.push(new Object());

Android内存泄漏问题

myStack.pop();// 弹出对象
myStack.pop();

Android内存泄漏问题
自定义栈代码展示了开发者手动维护对象内存出现的问题,Java中存在大量高效可靠的集合类型,它们极大地提高了程序员的开发效率,不过在使用集合类型时也要注意它们的特性,防止出现内存泄漏问题。下面的代码展示了简单的自定义对象Node,它在加入某个集合之后修改了内容,开发者再次使用它的引用将它从集合对象中删除。

static class Node implements Comparable<Node>{
    private int data;
    @Override
    public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
        Node node = (Node) o;
        return data == node.data;
    }
    @Override
    public int hashCode() {
        return Objects.hash(data);
    }
    @Override
	public int compareTo(Node o) {
    	return data - o.data; // 不考虑o为null的情况
    }
}

// 测试ArrayList
Node node1 = new Node(10);
List<Node> list = new ArrayList<>();
list.add(node1); node1.setData(15); list.remove(node1);
System.out.println("list = " + list.toString());
// 测试HashMap
Node node2 = new Node(20);
Map<Node, String> map = new HashMap<>();
map.put(node2, "Hello"); node2.setData(25); map.remove(node2);
System.out.println(map.toString());
// 测试TreeSet和HashSet代码与上面类似

//~ 执行结果
list = []
map = {Node{data=25}=Hello} // 对象未删除
treeSet = []
hashSet = [Node{data=45}] // 对象未删除

上面代码中定义的Node对象它的hashCode()和equals()方法都已经被重写,确保了在equals()返回true时hashCode()的返回值一定时相同的,因为要测试具有排序功能的TreeSet,Node类也实现了Comparable接口。从运行代码的结果可以看出ArrayList和TreeSet内部的对象类型即使修改了数据值依然能够成功移除,但HashMap一旦加入的键值对象内容一旦发生变化就无法通过键值对象引用来删除,HashSet内部的对象内容修改后无法通过对象引用从集合中删除。

在ArrayList内部判定对象是相同的使用的是equals()方法,虽然加入的对象内容发生了改变,ArrayList引用的是同一个对象,它们equals()时一定时相同的,再使用同一个对象的引用删除时不会出现问题。TreeSet内部比较相同对象也使用的时equals()方法,它和ArrayList删除是一样的结果,不过TreeSet带有排序的功能,修改了值的对象位置依然还在旧值所在的位置,并没有发生任何位置调整,后续的操作会导致TreeSet的排序功能失效。

HashMap的工作需要依赖于hashCode(),在删除对应的键值时首先根据hashCode()确定对象被映射到的数组槽索引位置,接着从数组槽指向的链表或红黑树中删除对应的对象引用。修改键对象内容后的键值由于hashCode()改变,HashMap根本无法定位到它实际的位置,也就无法删除该对象。HashSet同样依赖于hashCode(),对象内容的修改导致hashCode()值改变自然也就无法删除加入的对象。

由此可见在Java集合对象中调用remove(Object)并不是一定能够将对象从集合中移除,有些对象内容发生了变化就无法被移除,在工作中如果发现有些集合内容不再需要需要即使调用clear()清空它内部的所有对象引用。

Native层资源

除了上面提到的活动线程、静态成员变量和集合框架外,还有很多系统资源比如文件、Cursor游标对象等用完之后要及时地调用close()关闭操作避免内存泄漏。文件其实时操作系统的资源,开发者在程序中打开文件,操作系统会在系统中记录文件的打开次数,开辟内存缓存资源,没有及时关闭文件就会导致系统资源的浪费。Cursor游标通常在数据库请求时会用到,Android系统中为了能够方便跨进程访问会在底层开辟一块被CursorWindow对象管理的共享内存块,所有数据查询都会通过fillWindow()方法填充到Cursor Window管理的内存块中。

// SQLiteCursor.java 
public class SQLiteCursor extends AbstractWindowedCursor { 
// SQLiteCursor就是Cursor接口的实现
    // 省略不重要代码
    private void fillWindow(int requiredPos) {
        clearOrCreateWindow(getDatabase().getPath()); // 创建新的CursorWindow对象
		// 省略填充代码
    }

    @Override
    public void close() {
        super.close();
        synchronized (this) {
            mQuery.close();
            mDriver.cursorClosed(); // 关闭CursorWindow对象
        }
    }
}
// Native层CursorWindow实现android_database_CursorWindow.cpp
static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) {
    CursorWindow* window; // C++中使用new创建了CursowWindow
    status_t status = CursorWindow::create(name, cursorWindowSize, &window);
    return reinterpret_cast<jlong>(window);
}
tatus_t CursorWindow::create(const String8& name, size_t size, 
CursorWindow** outCursorWindow) {
	// 省略其他代码,使用new创建了CursorWindow对象
    CursorWindow* window = new CursorWindow(name, ashmemFd,
                            data, size, false /*readOnly*/);
    result = window->clear();       
    *outCursorWindow = window;
    return OK;              
}
// close()后才会删除Window对象调用析构函数
static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) {
    CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
    if (window) {
        delete window;
    }
}

上述代码中展示了Native层通过new操作符创建了CursorWindow对象,C++语言并不具备自动GC操作,需要程序员手动释放请求的内存,如果Java层的开发者没有调用Cursor.close()方法就不会删除CursorWindow对象在Native层分配的资源,导致整个系统中出现内存泄漏。