内存溢出(OOM)是指应用系统中存在无法回收的内存或使用的内存过多,使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件。
内存溢出(Out Of Memory,简称 OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
简介
内存溢出已经是软件开发历史上存在了近 40 年的“老大难”问题,像在“红色代码”病毒事件中表现的那样,它已经成为黑客攻击企业网络的“罪魁祸首”。 如在一个域中输入的数据超过了它的要求就会引发数据溢出问题,多余的数据就可以作为指令在计算机上运行。据有关安全小组称,操作系统中超过 50%的安全漏洞都是由内存溢出引起的,其中大多数与微软的技术有关。内存溢出错误是大数据处理平台的常见错误,例如,国际知名的程序开发者问答网站 stackoverflow 上关于“Hadoop out of memory”的问题超过 10000 个,在 Spark 邮件列表上有 10%的问题是关于“out of memory”。 内存溢出错误会导致处理数据的任务失败,甚至会引发平台崩溃等严重后果。对于内存溢出大部分的处理方法是重新执行任务,然而, 对于由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
内存溢出通俗理解就是内存不够,是指运行程序时要求的内存,超出了系统所能分配的范围,从而导致发生内存溢出。一般在运行大型软件时,所需的内存远远超出了主机内安装的内存所承受大小时就会发生这种情况。
当出现内存溢出这种情况,系统一般会提示相关信息,有时候会自动关闭软件甚至会造成设备卡死等现象,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件或游戏一段时间。
常见现象
以 Android 开发为例,在开发过程中经常遇到 Android 内存溢出的意外情况的发生。
以下是国内外总结造成内存溢出的几点现象。
1.大量位图的加载
Bitmap 代表一张位图文件,扩展名是.bmp 或者.dip,它是非压缩格式,其显示效果较好,但缺点就是需要占用大量的存储空间。它是 windows 标准格式图形文件,由点组成,每一个点代表一个像素。每个点可以由多种色彩表示,包括 2、4、8、16、24 和 32 位色彩。色彩越高,显示效果越好,但所占用的字节数也就越大。计算一张 Bitmap 所占内存大小主要由 3 个因数有关,即图片宽度,图片长度,单位像素所占用的字节数。大小=图像长度*图片宽度*单位像素占用的字节数。有时候我们需要从网络上获取大量的图片并且展现在 view 中,但是如果图片较大,一次性加载大量 Bitmap,那么程序可用内存会瞬间增长,引起 OOM。
2.位图对象没有及时释放
当程序中需要操作 Bitmap 对象的时候,当它不在被使用的时候,可以调用 Bitmap.recycle()方法回收此对象的像素所占用的内存,如果对 Bitmap 没有及时释放,在程序长期运行过程中,就很有可能造成 OOM 意外情况的发生。
3.查询数据库没有关闭游标
程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
4.构造 Adapter 时,没有使用缓存的 convertView
以构造 ListView 的 BaseAdapter 为例,在 BaseAdapter 中提高了方法: publicView getView(int position, View convertView, ViewGroup parent)来向 ListView 提供每一个 item 所需要的 view 对象。初始时 ListView 会从 BaseAdapter 中根据当前的屏幕布局实例化一定数量的 view 对象,同时 ListView 会将这些 view 对象缓存起来。当向上滚动 ListView 时,原先位于最上面的 list item 的 view 对象会被回收,然后被用来构造新出现的最下面的 listitem.这个构造过程就是由 getView()方法完成的,getView()的第二个形参 View convertVicw 就是被缓存起来的 listitem 的 view 对象(初始化时缓存中没有 view 对象则 convertView 是 null)。如果我们不去使用 convertView,而是每次都在 getView()中重新实例化一个 View 对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。
原因
造成这种现象的原因通常有两种:
第一种是由于长期保持某些资源的引用,垃圾回收器无法回收它,从而使该资源不能够及时释放,也称为内存泄露;
另外一种是当需要保存多个耗用内存过大或当加载单个超大的对象时,该对象的大小超过了当前剩余的可用内存空间。
以 Android 程序为例:
1.由强引用造成的内存溢出
若所有的引用都是强引用,则大量内存会被占用,最终导致内存溢出。
解决方法:使用弱引用或软引用,软引用的对象在内存不足时可被 GC 回收,弱引用的对象在垃圾回收时可被回收。
2.由大量图片显示导致的内存溢出
为解决由大量图片显示造成的内存溢出,可以使用 BitmapFactory.Options 类,在返回参数时,只返回 Bitmap 的尺寸大小,而不将其加载到内存中,可有效减少内存溢出。同时在加载完后调用 system. gc()通知系统及时回收。
3.从数据库中取出大量数据造成的内存溢出
检查在数据库查询中,是否有一次获得全部数据的查询。一般而言,如果一次取十万条记录到内存,就可能引起内存溢出。该问题比较隐蔽,在上线前,数据库中数据较少,通常运行正常,上线后,数据库中数据增多,一次查询即有可能引起内存溢出。因此,对于数据库查询,尽量采用分页的方式查询。
4.代码中存在死循环或循环产生过多重复对象实体造成的内存溢出
出现这种情况,只能通过查看日志找出产生该问题的原因,检查代码中是否有死循环、递归调用,或大循环重复产生的新对象实体。
解决方法
内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。以 Java 程序为例:
第一步,就是修改 JVM 启动参数,直接增加内存。这一点看上去似乎很简单,但很容易被忽略。JVM 默认可以使用的内存为 64M,Tomcat 默认可以使用的内存为 128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx 参数一定不要忘记加。
第二步,检查错误日志,查看“Out Of Memory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库连接,其中专用于发送短信的数据库连接使用 DBCP 连接池管理,用户为不将短信发出,有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志,一段时间后,就出现“Out Of Memory”错误。经分析,这是由于 DBCP 连接池 BUG 引起的,数据库连接不上后,没有将连接释放,最终使得 DBCP 报“Out Of Memory”错误。经过修改正确数据库连接参数后,就没有再出现内存溢出的错误。
查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。
第三步,安排有经验的编程人员对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查 List、MAP 等集合对象是否有使用完后,未清除的问题。List、MAP 等集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。
第四步,使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。
内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbeProfiler、JinSight 和 Java1.5 的 Jconsole 等。它们的基本工作原理大同小异,都是监测 Java 程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。
通过以上四个步骤的分析与处理,基本能处理内存溢出的问题。当然,在这些过程中也需要相当的经验与敏感度,需要在实际的开发与调试过程中不断积累。
避免内存溢出
避免内存溢出的常用方法众所周知,以 Android 开发为例,每个 Android 应用程序在运行时都有一定的内存限制,限制大小一般为 16MB 或 24MB(视平台而定)。当应用程序在实际运行过程中没有做到合理、有效利用内存空间,超过该限制大小就会内次溢出。
下面是列举了国内外在 Android 应用程序开发过程中应对内存溢出而经常采用的方法。
相关概念
内存泄露
内存泄露是造成内存溢出的其中一个原因,但是内存泄露不一定会造成内存溢出。简单来说,内存溢出就是占用内存太大,超过了系统可以承受的范围;而内存泄露则是由于对程序运行分配的对象回收不及时甚至于脆没有被回收,久而久之,则在系统分配的堆空间里面产生了很多无用的引用。
这种情况下,系统配置容量再多的内存空间都有可能发生内存溢出。当 Android 中 Dalivk 启动 GarbageCollection(GC)机制进行垃圾回收的时候,GC 会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说 thread stack 中的变量, JNI 中的全局变量,zygote 中的对象(class loader 加载)等,然后开始对 heap 进行遍历。到最后,部分没有直接或者间接引用到 GC Roots 的就是需要回收的垃圾,会被 GC 回收掉。GC 只能回收那么没有被引用的对象,如果一直引用,当遍历的时候,系统会默认为该对象仍然处于使用过程中,GC 无法回收供其他再次分配使用,但实际上这些被引用的对象对当前应用程序来说,是没有任何意义的,使得实际上可用的内存空间逐渐缩小。
以发生的方式进行分类,内存泄露可以具体分为如下几类:
(1) 偶发性内存泄露对造成内存泄露的代码只是在某些特定的环境或者操作过程下才会发生。一般情况下不会发生这种现象。
(2)常发性内存泄露对造成内存泄露的代码会被多次执行,每次被执行的时候都会导致一块内存泄露。偶发性和常发性内存泄露是相对而言的。对于使用不同的测试工具和测试算法,常发性可能会变成偶发性内存泄露,或者偶发性内存泄露也会变成常发性内存泄露。
(3)一次性内存泄露对造成内存泄露的代码只会被执行一次。比如,在初始化阶段,在类的构造函数中分配内存,但是在执行结束的阶段没有释放该内存,从而造成内存泄露的意外情况发生。
(4)隐式内存泄露程序在运行过程中不停地分配内存,但是没有及时释放,而是再等到执行结束的时候才会释放内存,严格地说,这种情况就会中造成内存泄露的发生。例如对于一个服务器,需要运行的时间很长,可能会长达几百天,如果存在隐式内存泄露,在最坏情况就会发生内存泄露。
从用户使用应用程序的角度来看,内存泄露是一种常见的现象,它本身不会产生非常重大的危害,甚至有一部分用户根本感觉不到内存泄露的发生。但是真正危害之处在于,这种内存泄露现象的堆积,最终会消耗尽系统所有的内存。因此,具有良好的编程习惯和采取严格的软件测试对于避免内存泄露是一种非常有效的方式。