Android开发中常常会遇到out of memory error 异常,并且这种异常的发生原因是比较难发现的,因为这种异常往往不是必现的。在一个手机上出现,换个手机有可能又不出现,在同一个手机也不是必现。这篇文章我们来探讨一下Android系统在什么情况下会抛这个异常,并且我们该怎么避免这个异常
1、oom发生的时机
art(Dalvik)虚拟机会为app设置堆内存使用上限,当app剩余可使用的内存小于申请的内存就会触发out of memory error。还有一种情况就是在创建线程时,创建线程时有三种情况会触发oom。在创建线程都需要先申请一段内存作为该线程的栈空间,一旦整个系统剩余的内存空间不足于提供创建线程所需要的内存空间时就会发生oom。这种情况和虚拟机触发的oom不同,虚拟机是app进程的堆内存耗尽时就会触发oom,这个时候有可能系统还有很多剩余的内存,而创建线程这种是整个系统的内存都被用完了才会发生oom;一般来说Android中主线程需要的栈空间是8M,其他子线程默认需要的栈空间是1m,也可以在创建子线程是指定栈的大小
/* @param group
* the thread group. If {@code null} and there is a security
* manager, the group is determined by {@linkplain
* SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
* If there is not a security manager or {@code
* SecurityManager.getThreadGroup()} returns {@code null}, the group
* is set to the current thread's thread group.
*
* @param target
* the object whose {@code run} method is invoked when this thread
* is started. If {@code null}, this thread's run method is invoked.
*
* @param name
* the name of the new thread
*
* @param stackSize
* the desired stack size for the new thread, or zero to indicate
* that this parameter is to be ignored.
*
* @throws SecurityException
* if the current thread cannot create a thread in the specified
* thread group
*
* @since 1.4
*/
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
第四个参数就是指定栈的大小。可以粗略的计算一下,在一个32位的手机中默认最多可以创建的线程数为:(2^32 - 8 * 1024 * 1024) / (1024 * 1024),实际上最大的线程数肯定会小于这个值,因为除了上述所说的内存的限制,linux内核对最大线程数也做了限制,这个数值一般是由手机厂商定制的,根据我遇到的手机,发现华为的手机这个值设置的相对较小,大概是500左右。创建线程的第二个发生oom的地方就是当线程数大于系统限制的最新线程数量时,linux创建线程需要创建一个FileDescriptor,当进程中的fd数量达到最大值时也会触发oom
2、减少oom的一些建议
上一节说了发生oom的时机,这一节来说说如果减少发生oom的概率。对于由于堆内存不足虚拟机触发的oom,我们应该及时回收不再使用的对象,避免造成内存泄漏,如果发生内存泄漏可以做一些补救措施,如何补救请看参考我的另一篇文章关于内存泄漏的一些思考。
Android中最消耗的内存就是bitmap,bitmap内存由两部分组成,一部分是bitmap对象所需要的内存,这部分所需内存一般不大和其他对象差不多,另一部分是保存图片资源的byte数组,在Android3.0之前的系统中这部分内存放在native中,这是不占用堆内存的。但是3.0之前是在bitmap
* @throws Throwable the {@code Exception} raised by this method
* @see java.lang.ref.WeakReference
* @see java.lang.ref.PhantomReference
* @jls 12.6 Finalization of Class Instances
*/
protected void finalize() throws Throwable { }
中释放图片所以占用的byte数组,由于finalize方法执行时机的不确定性可能导致bitmap对象被销毁之后,图片资源还很长一段时间没有被释放,这种情况必须在确定bitmap不再需要被使用时手动调用bitmap的recyle方法。
正是由于这个原因Android3.0之后系统把图片资源的byte数组直接放到了应用进程的堆中,当然由于堆内存的大小有限制可能会导致在系统还有很多内存时发生oom。在Android8.0之后又把资源的byte数组放回到native中,这次Android系统用了一中比较厉害的机制来保证图片资源及时被回收,这个机制具体是怎么运作的目前没有研究。
我们的手机的显示区域有限,很多时候不需要把整张图片都加载到内存中,可以根据展示区域的大小来对图片进行采样
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder uses a final value based on powers of 2, any other value will
* be rounded down to the nearest power of 2.
*/
public int inSampleSize;
通过设置inSampleSize来决定图片的采样率,这个值只能是2的n次方
监测app的剩余堆内存,当剩余的堆内存达到某个阀值时释放app的缓存。还可以在app统一bitmap创建的入口,根据不同的手机来决定要创建bitmap的像素格式,高端手机中可以使用argb8888,低端一点的手机使用rgb565,同样大小的图片argb8888是rgb565的两倍
对于创建线程导致的oom,可以在app中用统一的线程池,统一的ThreadFactory。利用 linux 的 inotify 机制进行监控:
watch /proc/pid/fd来监控 app 打开文件的情况,
watch /proc/pid/task来监控线程使用情况.