【性能】Android中的内存溢出(Out Of Memory,OOM)

1 JVM内存区域介绍

一般来说,应用创建时会给其分配一个虚拟机,应用的中几乎所有的数据都存储在虚拟机的内存区域,而虚拟机的内存区域又分为5大块,分别是:Java堆,方法区,程序计数器,虚拟机栈和本地方法栈,借用一张图:
在这里插入图片描述

  • Java堆(Java Heap)
    Java堆数据线程共享区域,它在虚拟机启动时创建,是JVM中最大的一块内存区域。主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java堆是垃圾回收的主要区域,因此很多地方也被称为GC堆。

  • 方法区(Method Area)
    方法区属于线程共享区域,又称非堆(Non-Heap),主要用于存放被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码数据。方法区中存在一个叫运行时常量池的区域,它主要存放的是编译器生成的各种字面量和符号引用,这些内容将在类加载后存放在运行时常量池中,以便后续使用。

  • 程序计数器
    属于线程私有的数据区域,是很小的一块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释工作时,通过改变程序计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  • 虚拟机栈
    属于线程私有的数据区域,与线程同时创建,总数与线程数量相关,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈帧来存储方法的变量表,操作数栈,动态链接方法,返回值,返回地址等信息。每个方法从调用到结束就对应于一个栈帧的入栈和出栈过程,如下:
    在这里插入图片描述

  • 本地方法栈
    本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

2. OOM形成的原因

OOM形成的原因是无法申请到足够的内存空间

3. 造成OOM的有哪些

3.1 从JVM的角度

  • Java堆
    我们知道,Java堆是用于存储对象实例的,当我们不停的new对象,然后保证new出来的对象到GC root之间有可达路径,则一段时间后,当Java堆达到了可用的最大容量后,且Java堆无法再进行扩展时,就会抛出OOM异常。而在android中,内存泄漏可能造成上述情况。内存泄漏是长生命周期持有短生命周期,导致短生命周期无法及时释放。所以解决内存泄漏有利于缓解OOM的问题。

  • 虚拟机栈
    虚拟机栈存储的是栈帧,每个栈帧中存储着局部变量表,操作数栈、动态链接方法,返回值,返回地址等信息。如果采用固定大小的虚拟机栈,那么每个Java虚拟机栈容量在线程创建时独立选定,如果申请的虚拟机栈容量大于虚拟机允许的最大栈容量,则会抛出StackOverFlowError异常;如果虚拟机栈可以动态扩展,则虚拟机扩展时申请的内存空间大于虚拟机栈最大允许的内存空间,则会抛出OutOfMemoryError。

  • 方法区
    方法区属于线程共享区域,存储着被虚拟机加载的类信息,常量,静态变量和及时编译器编译后生成的代码数据。当方法区无法满足内存需求时,这块内存也有可能抛出OutOfMemoryError异常。

  • 本地方法栈
    和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError。

3.2 从具体使用角度

Android使用过程中造成的内存溢出可以归结几大类

  • 内存泄漏导致的内存溢出
  • 资源使用不合理导致的内存溢出

下面来具体说明以上两点

3.2.1 内存泄漏导致的内存溢出

我们都知道,内存泄漏的原因是长生命周期持有短生命周期的对象,导致短生命周期对象无法被及时回收。再说具体点,就是应该被回收的对象因为被强引用导致到GC root之间有可达路径,因此不能被回收。所以内存泄漏可能导致Java堆内存一直无法释放,最终程序无法申请到足够的堆内存来存储新创建的对象,导致OutOfMemoryError异常。所以,这部分主要关注的是内存泄漏,解决或避免了内存泄漏,将大大减少内存溢出的概率,而内存泄漏包括:

(1)单例造成的内存泄漏
我们一般用单例模式来对类的对象进行控制,使其全局仅有一个对象。但是,有时候单例模式创建对象时,对象并不是都是无参的,有时需要注入对象类型参数,而且对象内部可能还要初始化一些其它的对象类型。因为单例创建的对象是静态的,与Application的生命周期一样长,导致了单例里面的对象可能一直无法得到释放,这样,就可能导致内存泄漏问题。以下是双重校验单例示例:

public class SingletonTest {
	private volatile static SingletonTest singleton;
	private Bean bean;
	//省略

	private SingletoTest(A a,B b){
		C c = new C();
		D d = getXXXD();
		//省略
	}

	public SingletonTest getInstance(){
		if(singleton == null){
			synchronized(this){
				if(singleton == null){
					singleton = new SingletonTest(a,b);
				}
			}
		}
		return singleton;
	}
	
}

可以从上面看到,在单例模式的类SingletonTest的对象是静态的,也就是说,它内部所有强引用的对象在SingletonTest对象没有被销毁时是无法被回收的,导致了其它对象无法及时被回收,从而导致了内存泄漏。单例造成的内存泄漏并不仅仅是传入context时可能造成内存泄漏,而如果单例需要传入Context对象,应该使用Application的Context。所以需要谨慎使用单例模式。

(2)非静态内部类实例化为一个静态的对象
非静态内部类隐性持有外部类的对象,如果将内部类实例化为一个静态对象,那么它将与Application拥有一样长的生命周期,如果外部类需要销毁,由于内部类持有该类的引用,所以无法销毁,从而造成内存泄漏

public class MyActivity extends Activity{
    private static InnerClass innerclass = new InnerClass();
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(saveInstanceState);
        
    }
    private class InnerClass{
    }
}

(3) Handler/Runnable造成的内存泄漏
Handler是一个非静态内部类,它隐性地持有外部类的对象。如果外部类需要结束,但消息队列中还有消息未处理完,则Handler不会释放外部类的对象,从而造成内存泄漏,代码示例如下:

Handler handler = new Handler(){
    public void handleMessage(Message msg){
        super.handleMessage(msg);
    }
};

解决方法:

  • 将Handler/Runnable改为静态的,如果是Handler,则在onDestroy中调用removeCallbackAndMessage(null)方法。
  • 如果Handler/Runnable持有外部类的对象,则应该改成弱引用。

(4)资源未关闭造成的内存泄漏
如使用流资源,比如InputStream,Database等,还有就是BroadcastReceiver未取消注册等。
(5)静态集合类的使用导致内存泄漏
有时候会需要使用集合类,如ArrayList,HashMap等,用集合类来存储对象,如果集合类被声明是静态的,且使用完集合类后没有进行清除,可能导致内存泄漏。

3.2.2 资源使用不合理导致内存溢出

(1)使用大图
有时候使用背景图或其它图片资源时,选用图片大小不合理,往往超出了实际控件的大小,这时我们就需要选择合适的图片尺寸和图片分辨率,来避免不必要的内存消耗。
(2)Bitmap等对象类型没有复用
复用对象类型也可以有效减少内存的占用,如Bitmap,Bitmap是一个很消耗内存的一个对象,减少创建出来的Bitmap占用的内存很重要,方法有:

  • inSimpleSize:缩放比例,在图片载入内存之前,我们需要选择合适的尺寸,避免不必要的内存占用
  • decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
    本人一般选择使用第三方图片加载库。还有一个就是复用Message,Message是以消息池的方式管理消息的,它是一个链表结构。可以通过Message.obtain方法获取消息池中的Message对象从而达到复用。

(3)可以考虑复用系统自带资源
可以考虑复用系统自带资源,如图片、布局、尺寸、颜色、动画、属性等。
(4)避免在Android内随意使用枚举(Enum)
枚举类型编译后会生成一个枚举类,枚举类型里面每个参数都是一个对象,而且它编译后还会生成一个数组类型,占用空间内存比较大。
(5)ListView/GridView等未复用ConvertView布局
ListView/GridView等列表布局,多数情况下其Item项的布局都是重复的,这时,我们应该考虑复用这些布局,而不是每次加载就去创建。
(6)避免在onDraw里面创建对象
类似onDraw这种频繁调用的方法,要避免在里面创建对象。

参考文章

  1. 虚拟机栈—JVM(五)
  2. 全面理解Java内存模型(JMM)及volatile关键字
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
测试程序的内存泄漏内存溢出OOM(Out of Memory)和ANR(Application Not Responding)是为了确保程序在使用内存和响应用户输入时的稳定性和可靠性。 内存泄漏是指程序已经不再使用的内存没有被正确释放,导致内存的占用不断增加,最终可能导致程序崩溃。为了测试内存泄漏,可以创建一个长时间运行的程序,并通过监测内存使用情况来判断是否有内存泄漏。可以使用内存分析工具来检测未被垃圾回收器回收的对象,以及通过分析堆转储文件来查找内存泄漏的源头。 内存溢出是指程序在申请内存时,无法分配到足够的内存空间,导致程序崩溃。为了测试内存溢出,可以通过申请大量的内存空间来触发溢出,或者通过无限制地生成对象导致内存快速占满。可以使用性能测试工具来模拟大量并发请求和数据量,以模拟真实环境的内存使用情况,从而找出内存溢出的问题。 OOM是指由于内存不足导致程序无法继续分配内存空间而崩溃。为了测试OOM,可以通过限制程序可用内存的上限,观察程序在分配内存时是否能够正常运行,当内存达到上限时,是否能够优雅地处理内存不足的情况。 ANR是指应用程序无法在规定的时间内响应用户的输入事件,导致系统认为应用程序无响应而弹出ANR对话框。为了测试ANR,可以创建一个需要执行较长时间的代码块,来模拟应用程序无法及时响应用户输入的情况。可以通过监测主线程的响应时间来判断是否出现ANR。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值