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

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

程序员文章站 2022-06-21 19:22:10
【性能】Android中的内存溢出(Out Of Memory,OOM)1 JVM内存区域介绍2. OOM形成的原因3. 造成OOM的有哪些3.1 从JVM的角度3.2 从具体使用角度3.2.1 内存泄漏导致的内存溢出3.2.2 资源使用不合理导致内存溢出参考文章1 JVM内存区域介绍一般来说,应用创建时会给其分配一个虚拟机,应用的中几乎所有的数据都存储在虚拟机的内存区域,而虚拟机的内存区域又分为5大块,分别是:Java堆,方法区,程序计数器,虚拟机栈和本地方法栈,借用一张图:Java堆(Jav...

1 JVM内存区域介绍

一般来说,应用创建时会给其分配一个虚拟机,应用的中几乎所有的数据都存储在虚拟机的内存区域,而虚拟机的内存区域又分为5大块,分别是:Java堆,方法区,程序计数器,虚拟机栈和本地方法栈,借用一张图:
【性能】Android中的内存溢出(Out Of Memory,OOM)

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

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

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

  • 虚拟机栈
    属于线程私有的数据区域,与线程同时创建,总数与线程数量相关,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈帧来存储方法的变量表,操作数栈,动态链接方法,返回值,返回地址等信息。每个方法从调用到结束就对应于一个栈帧的入栈和出栈过程,如下:
    【性能】Android中的内存溢出(Out Of Memory,OOM)

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

2. OOM形成的原因

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

3. 造成OOM的有哪些

3.1 从JVM的角度

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

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

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

  • 本地方法栈
    和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出*Error 和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关键字

本文地址:https://blog.csdn.net/u013293125/article/details/107119809