OOM全称是Out Of Memory,指的是kernel因分配不出内存而报的错误,同时会触发kernel调用OOM killer杀进程来解除这种状况。
OOM发生的条件一般有两个:
下面通过分析linux kernel中oom_kill.c代码来了解一下OOM的机制。OOM在kernel中对应的函数有两个:out_of_memory()和pagefault_out_of_memory(),最终调用的都是__out_of_memory()。
__out_of_memory()做两件事情:
2) 对其它的进程调用badness()函数来计算相应的score,score最高的将被选中。badness()函数计算score (points)的因子有下面几个:
a) score起始为该进程占用的total_vm;
points = mm->total_vm;
b) 如果该进程有子进程,子进程独自占用的total_vm/2加到本进程score;
points += child->mm->total_vm/2 + 1;
c) score和进程的cpu_time以及run_time成反比;
points /= int_sqrt(cpu_time);
points /= int_sqrt(int_sqrt(run_time));
d) nice大于0的进程,score翻倍;
if (task_nice(p) > 0) points *= 2;
e) 对设置了超级权限的进程和直接磁盘交互的进程降低score;
if (CAP_SYS_ADMIN | CAP_SYS_RESOURCE | CAP_SYS_RAWIO) points /= 4;
f) 如果和current进程在内存上没有交集的进程降低score;
if (!has_intersects_mems_allowed(p)) points /= 8;
g) 最后是根据该进程的oom_adj计算最终的score;
points «= abs(oom_adj);
oom_kill_process函数的功能很简单,就一句话:
force_sig(SIGKILL, p);
可以看到发的是SIGKILL信号,其实就是执行kill -9 pid,因为SIGKILL是不能被捕获的。
可以通过下面两个参数来配置OOM策略:
/proc/sys/vm/overcommit_memory
/proc/sys/vm/overcommit_ratio
overcommit_memory取值为[0-2]:
0:表示按启发模式进行overcommit(可以提交超过物理内存大小的alloc page申请),也是默认的设置;
1:表示总是允许overcommit,这种模式最容易触发oom;
2:表示不能overcommit。这种模式下,最大的User Space限制在:SS + RAM*(r/100),SS是swap大小,r就是overcommit_ratio设置的值,范围为:[0-100]。
有一种mem_notify的机制在内存不足时可以给应用进程发信号,让应用进程去释放内存,如果不能释放再调用oom killer,但在linux 2.6.28以后的版本都不能用了,所以避免OOM还是做好应用的内存管理以及监控。
参考资料:
1. http://lxr.linux.no/linux+v2.6.32.60/mm/oom_kill.c
2. http://lwn.net/Articles/267013/
3. http://www.kernel.org/doc/man-pages/online/pages/man5/proc.5.html
OOM为out of memory的简称,称之为内存溢出。
程序中常见的打印有如下几类:
一:
如图:
Java应用程序在启动时会指定所需要的内存大小,其主要被分割成两个不同的部分,分别为Head space(堆空间-Xmx指定)和Permegen(永久代-XX:MaxPermSize指定),
通常来说,造成如上图异常的基本上程序代码问题而造成的内存泄露。这种异常,通过dump+EMA可以轻松定位。(EMA虽功能强大,但对机器性能内存要求极高)
二:
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
如上异常,即程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高。定位方法同上。
三:
Java.lang.OutOfMemoryError: PermGen space(JAVA8引入了Metaspace区域)
永久代内存被耗尽,永久代的作用是存储每个类的信息,如类加载器引用、运行池常量池、字段数据、方法数据、方法代码、方法字节码等。基本可以推断PermGen占用大小取决于被加载的数量以及类的大小。定位方法同上。
还有很多OOM异常,甚至会触发操作系统的OOM killer去杀掉其它进程。
四:
本节主要讨论下面一种OOM,如图
产生这种异常的原因是由于系统在不停地创建大量的线程,且不进行释放。系统的内存是有限的,分配给JAVA应用的程序也是有限的,系统自身能允许创建的最大线程数计算规则:
(MaxProcessMemory-JVMMemory-ReservedOsMemory)/ThreadStackSize
其中
MaxProcessMemory:指的是一个进程的最大内存
JVMMemory :JVM内存
ReservedOsMemory:保留的操作系统内存
ThreadStackSize:线程栈的大小
从公式中可以得出结论,系统可创建线程数量与分配给JVM内存大小成反比。
在java程序中,创建一个线程,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,且系统线程的内存不占用JVMMemory,而占用系统中剩下的内存,即(MaxProcessMemory - JVMMemory - ReservedOsMemory)。
结论很明显:程序创建的线程不可能无限大。
先讨论第一种情况,即经jstack或dump后,线程的数量确在系统要求的阀值内,报上面异常,该如何?
1.参考之前的参数,可以修改两个变量JVMMemory和ThreadStackSize来满足更多线程创建的要求。
-Xss1024k -Xms4096m -Xmx4096m
2.查看是否操作系统限制了可创建线程数量
执行ulimit -u 可以查看当前用户可创建线程量,如果不满足要求,可以通过修改配置文件调整其大小:
相关配置文件在etc/security/limit.d/XX-nproc.conf中
由于上面配置不合理造成而产生异常,个人认为应属小概率事件。
第二种情况,是程序中存在线程泄露,代码本身有问题,在某些场景,条件下存在线程不断创建而不销毁的BUG。
先看一段代码(取自业务真实代码稍加改写):
package com.zte.sunquan.demo.netty.server;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainController {
private ExecutorService executor = Executors.newSingleThreadExecutor();
private static Map<String, MainController> map = Maps.newConcurrentMap();
public void submitTask() {
for (int i = 0; i < 10; i++) {
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “正处理”);
}
});
}
}
public static void main(String[] args) throws InterruptedException {
while (true) {
MainController ct1 = new MainController();
map.put(“1”, ct1);
ct1.submitTask();
map.remove(“1”);
}
}
}
如上表的程序就存在线程泄露,开发人员误以为从map中移除,对象就可以释放,最终交由GC回收,但其实在创建的对象中创建的线程池,由于未关闭,执行完任务后,进入waiting状态,是不会释放的,随着程序运行,线程会越积越多,最终导致OOM的异常。
又或者将main函数,改为一个监听入口,在一般甚至短暂的压力测试中,虽然线程较多,但系统仍可以正常进行,误以为系统运行正常,但大业务、多节点、网络震荡、长时间,商用等能够大量触发该监听的场景中,这类问题才以最终kill系统进程暴露出来。
此外,如上表中的代码,线程池创建的进程,采用Executors中DefaultThreadFactory提供的pool-XXX-thread-XXX命名规则,诚然可以通过jstack打印线程调用栈,但如下面的打印:
…..
“pool-2577-thread-1” #8681 prio=5 os_prio=0 tid=0x00007f8ea4de7800 nid=0x6a0b waiting on condition [0x00007f8cae227000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
“pool-2576-thread-1” #8680 prio=5 os_prio=0 tid=0x00007f8ea4de6000 nid=0x6a0a waiting on condition [0x00007f8cae72c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
完整打印一直是从pool-1-thread-1到pool-5406-thread-1,很明显代码一直在创建线程池,但从打印输出,却无法分析出罪魁祸首是谁?接下来自然,会去系统日志中搜索pool-开头的线程打印,期望能找到线索,遗憾的是,什么信息也没有。此时问题定位,卡壳了。但可以肯定,代码肯定有前面例子中类似的写法。此时,走查代码,不失为一个好办法。
线索就此断掉了。。。。。
但考虑到是使用JDK并发包提供的功能,那在JDK中Executors类中,创建线程时,增加打印,强制将调用栈的信息打印出来,是否可以找到蛛丝马迹?
确定JDK版本—>获取源码—->修改代码—>编译——–>合入rt.jar ——->复现
我们将最终信息输入到了var/log目录下,需要提前将该目录的权限开放chmod 777 -R
输出日志中,对一些连续创建的线程,观察打印信息,幸运女神出现了,OpticalTopologyAdapter,抓到你了。问题解决!
这种问题的出现,归根到底,是没有对线程池进行管理,开发人员对于线程池的滥用,影响了程序稳定性。
减小对象的内存占用
避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。
1)使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。图8演示了HashMap 的简要工作原理,相比起Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap 的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与 value的自动装箱(autoboxing),并且避免了装箱后的解箱。
图8 HashMap简要工作原理
关于更多ArrayMap/SparseArray的讨论,请参考《 Android性能优化典范(三)》的前三个段落。
2)避免在Android里面使用Enum
Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考《Android性能优化典范(三)》,所以请避免在Android里面使用到枚举。
3)减小Bitmap对象的内存占用
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重,通常来说有以下2个措施:
inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4)使用更小的图片
在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不 仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会 因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。
内存对象的重复利用
大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用的实现逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收(如图9所示)。
图9 对象池技术
在Android上面最常用的一个缓存算法是LRU(Least Recently Use),简要操作原理如图10所示。
图10 LRU简要操作原理
1)复用系统自带的资源
Android系统本身内置了很多的资源,比如字符串、颜色、图片、动画、样式以及简单布局等,这些资源都可以在 应用程序中直接引用。这样做不仅能减少应用程序的自身负重,减小APK的大小,还可以在一定程度上减少内存的开销,复用性更好。但是也有必要留意 Android系统的版本差异性,对那些不同系统版本上表现存在很大差异、不符合需求的情况,还是需要应用程序自身内置进去。
2)注意在ListView/GridView等出现大量重复子组件的视图里对ConvertView的复用,如图11所示。
图11
3)Bitmap对象的复用
在ListView与GridView等显示大量图片的控件里,需要使用LRU的机制来缓存处理好的Bitmap,如图12所示。
图12
利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率(注:3.0以及4.4以后存在一些使用限制上的差异)。使 用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所 占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数 量的内存大小,如图13所示。
图13 利用inBitmap的高级特性提高Android在Bitmap分配与释放执行效率
使用inBitmap需要注意几个限制条件:
在SDK 11 -> 18之间,重用的Bitmap大小必须是一致的。例如给inBitmap赋值的图片大小为100-100,那么新申请的Bitmap必须也为 100-100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小。
新申请的 Bitmap与旧的Bitmap必须有相同的解码格式。例如大家都是8888的,如果前面的Bitmap是8888,那么就不能支持4444与565格式 的Bitmap了。我们可以创建一个包含多种典型可重用Bitmap的对象池,这样后续的Bitmap创建都能够找到合适的“模板”去进行重用,如图14 所示。
图14
另外,在2.x的系统上,尽管Bitmap是分配在Native层,但还是无法避免被计算到OOM的引用计数器 里。这里提示一下,不少应用会通过反射vBitmapFactory.Options里面的inNativeAlloc来达到扩大使用内存的目的,但是如 果大家都这么做,对系统整体会造成一定的负面影响,建议谨慎采纳。
4)避免在onDraw方法里面执行对象的创建
类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。
5)StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。
避免对象的内存泄露
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分 配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,容易出现内存抖动,从 而引起性能问题(如图15所示)。
图15
最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况,更多关于LeakCanary的介绍,请看 这里( 中文使用说明)。另外也可以使用传统的MAT工具查找内存泄露,请参考 这里( 便捷的中文资料)。
1)注意Activity的泄漏
通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的Activity泄漏:
内部类引用导致Activity的泄漏
最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。
Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。
2)考虑使用Application Context而不是Activity Context
对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。
3)注意临时Bitmap对象的及时回收
虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回 收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原 始bitmap所占用的空间。
需要特别留意的是Bitmap类里面提供的createBitmap()方法,如图16所示:
图16 createBitmap()方法
这个函数返回的bitmap有可能和source bitmap是同一个,在回收的时候,需要特别检查source bitmap与return bitmap的引用是否相同,只有在不等的情况下,才能够执行source bitmap的recycle方法。
4)注意监听器的注销
在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。
5)注意缓存容器中的对象泄漏
有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对象没有及时从容器中清除,也 是有可能导致内存泄漏的。例如,针对2.3的系统,如果把drawable添加到缓存容器,因为drawable与View的强应用,很容易导致 activity发生泄漏。而从4.0开始,就不存在这个问题。解决这个问题,需要对2.3系统上的缓存drawable做特殊封装,处理引用解绑的问 题,避免泄漏的情况。
6)注意WebView的泄漏
Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,请看 这里。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
7)注意Cursor对象是否及时关闭
在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。
内存使用策略优化
1)谨慎使用large heap
正如前面提到的,Android设备根据硬件与软件的设置差异而存在不同大小的内存空间,他们为应用程序 设置了不同大小的Heap限制阈值。你可以通过调用getMemoryClass()来获取应用的可用Heap大小。在一些特殊的情景下,你可以通过在 manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间。然后,你可以通过 getLargeMemoryClass()来获取到这个更大的heap size阈值。然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多 的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap。因此请谨慎使用large heap属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。
2)综合考虑设备内存阈值与其他因素设计合适的缓存大小
例如,在设计ListView或者GridView的Bitmap LRU缓存的时候,需要考虑的点有:
应用程序剩下了多少可用的内存空间?
有多少图片会被一次呈现到屏幕上?有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕?
设备的屏幕大小与密度是多少? 一个xhdpi的设备会比hdpi需要一个更大的Cache来hold住同样数量的图片。
不同的页面针对Bitmap的设计的尺寸与配置是什么,大概会花费多少内存?
页面图片被访问的频率?是否存在其中的一部分比其他的图片具有更高的访问频繁?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache容器。
3)onLowMemory()与onTrimMemory()
Android用户可以随意在不同的应用之间进行快速切换。为了让background的应用能够迅速的 切换到forground,每一个background的应用都会占用一定的内存。Android系统会根据当前的系统的内存使用情况,决定回收部分 background的应用内存。如果background的应用从暂停状态直接被恢复到forground,能够获得较快的恢复体验,如果 background应用是从Kill的状态进行恢复,相比之下就显得稍微有点慢,如图17所示。
图17 从Kill状态进行恢复体验更慢
onLowMemory():Android 系统提供了一些回调来通知当前应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到 onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。
onTrimMemory(int):Android 系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面 会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内 存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用。
TRIM_MEMORY_UI_HIDDEN:你的应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键退出应用,导致应用的UI界面完全不可见。这个时候应该释放一些不可见的时候非必须的资源
当程序正在前台运行的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:
TRIM_MEMORY_RUNNING_MODERATE:你的应用正在运行并且不会被列为可杀死的。但是设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache中的Process的机制。
TRIM_MEMORY_RUNNING_LOW:你的应用正在运行且没有被列为可杀死的。但是设备正运行于更低内存的状态下,你应该释放不用的资源用来提升系统性能。
TRIM_MEMORY_RUNNING_CRITICAL:你 的应用仍在运行,但是系统已经把LRU Cache中的大多数进程都已经杀死,因此你应该立即释放所有非必须的资源。如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进 程,并且开始杀死那些之前被认为不应该杀死的进程,例如那个包含了一个运行态Service的进程。
当应用进程退到后台正在被Cached的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:
TRIM_MEMORY_BACKGROUND: 系 统正运行于低内存状态并且你的进程正处于LRU缓存名单中最不容易杀掉的位置。尽管你的应用进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉 LRU缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的应用的时候才能够迅速恢复。
TRIM_MEMORY_MODERATE: 系统正运行于低内存状态并且你的进程已经已经接近LRU名单的中部位置。如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。
TRIM_MEMORY_COMPLETE: 系统正运行于低内存的状态并且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释放任何不影响你的应用恢复状态的资源。
因为onTrimMemory()的回调是在API 14才被加进来的,对于老的版本,你可以使用onLowMemory)回调来进行兼容。onLowMemory相当与TRIM_MEMORY_COMPLETE。
请注意:当系统开始清除LRU缓存中的进程时,虽然它首先按照LRU的顺序来执行操作,但是它同样会考虑进程的内存使用量以及其他因素。占用越少的进程越容易被留下来。
4)资源文件需要选择合适的文件夹进行存放
我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经 过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到 200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
5)Try catch某些大内存分配的操作
在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。
6)谨慎使用static对象
因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象(如图19所示)。
图19
7)特别留意单例对象中不合理的持有
虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。
8)珍惜Services资源
如果你的应用需要在后台使用service,除非它被触发并执行一个任务,否则其他时候 Service都应该是停止状态。另外需要注意当这个service完成任务之后因为停止service失败而引起的内存泄漏。 当你启动一个Service,系统会倾向为了保留这个Service而一直保留Service所在的进程。这使得进程的运行代价很高,因为系统没有办法把 Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在 运行的service。 建议使用 IntentService,它会在处理完交代给它的任务之后尽快结束自己。更多信息,请阅读 Running in a Background Service。
9)优化布局层次,减少内存消耗
越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。
10)谨慎使用“抽象”编程
很多时候,开发者会使用抽象类作为”好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的额外内存开销:他们需要同等量的代码用于可执行,那些代码会被mapping到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们。
11)使用nano protobufs序列化数据
Protocol buffers是由Google为序列化结构数据而设计的,一种语言无关,平台无关,具有良好的扩展性。类似XML,却比XML更加轻量,快速,简单。如果你需要为你的数据实现序列化与协议化,建议使用nano protobufs。关于更多细节,请参考 protobuf readme的”Nano version”章节。
12)谨慎使用依赖注入框架
使用类似Guice或者RoboGuice等框架注入代码,在某种程度上可以简化你的代码。图20是使用RoboGuice前后的对比图:
图20 使用RoboGuice前后对比图
使用RoboGuice之后,代码是简化了不少。然而,那些注入框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的内存空间来mapping代码,而且mapped pages会长时间的被保留在内存中。除非真的很有必要,建议谨慎使用这种技术。
13)谨慎使用多进程
使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内 存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能 反而会导致显著增加内存。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。
一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:一个用来操作UI,另外一个给后台的Service。
14)使用ProGuard来剔除不需要的代码
ProGuard能够通过移除不需要的代码,重命名类,域与方法等等对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够减少mapping代码所需要的内存空间。
15)谨慎使用第三方libraries
很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设 备上,并不一定适合。即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。 例如,其中一个library使用的是nano protobufs, 而另外一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样类似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。 另外不要为了1个或者2个功能而导入整个library,如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方 案。
16)考虑不同的实现方式来优化内存占用
在某些情况下,设计的某个方案能够快速实现需求,但是这个方案却可能在内存占用上表现的效率不够好。例如:
图21
对于上面这样一个时钟表盘的实现,最简单的就是使用很多张包含指针的表盘图片, 使用帧动画实现指针的旋转。但是如果把指针扣出来,单独进行旋转绘制,显然比载入N多张图片占用的内存要少很多。当然这样做,代码复杂度上会有所增加,这 里就需要在优化内存占用与实现简易度之间进行权衡了。
使用某云数据库时碰到mysql挂掉。技术人员说是因为内存耗尽触发OOM,导致系统干掉了mysql进程。具体查了下oom,以下为原文。
说明:
Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。典型的情况是:某天一台机器突然ssh远程登录不了,但能ping通,说明不是网络的故障,原因是sshd进程被OOM killer杀掉了(多次遇到这样的假死状况)。重启机器后查看系统日志/var/log/messages会发现Out of Memory: Kill process 1865(sshd)类似的错误信息。
防止重要的系统进程触发(OOM)机制而被杀死:可以设置参数/proc/PID/oom_adj为-17,可临时关闭linux内核的OOM机制。内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以/proc/PID/oom_score中找到。我们运维过程中保护的一般是sshd和一些管理agent。
保护某个进程不被内核杀掉可以这样操作:
如何防止sshd被杀,可以这样操作:
可以在计划任务里加入这样一条定时任务,就更安全了:
#/etc/cron.d/oom_disable
/1*** root pgrep -f “/usr/sbin/sshd” | while read PID;do echo -17 > /proc/$PID/oom_adj;done
为了避免重启失效,可以写入/etc/rc.d/rc.local
echo -17 > /proc/$(pidof sshd)/oom_adj
至于为什么用-17而不用其他数值(默认值为0),这个是由linux内核定义的,查看内核源码可知:
以linux-3.3.6版本的kernel源码为例,路径为linux-3.6.6/include/linux/oom.h,阅读内核源码可知oom_adj的可调值为15到-16,其中15最大-16最小,-17为禁止使用OOM。oom_score为2的n次方计算出来的,其中n就是进程的oom_adj值,所以oom_score的分数越高就越会被内核优先杀掉。
1
当然还可以通过修改内核参数禁止OOM机制
vm.panic_on_oom = 1 //1表示关闭,默认为0表示开启OOM
实验:
为了验证OOM机制的效果,我们不妨做个测试。
首先看看我系统现有内存大小,没错96G多,物理上还要比查看的值大一些。
2
再看看目前进程最大的有哪些,top查看,我目前只跑了两个java程序的进程,分别4.6G,再往后redis进程吃了21m,iscsi服务占了32m,gdm占了25m,其它的进程都是几M而已。
3
现在我自己用C写一个叫bigmem程序,我指定该程序分配内存85G,呵呵,效果明显,然后执行后再用top查看,排在第一位的是我的bigmem,RES是物理内存,已经吃满了85G。
4
继续观察,当bigmem稳定保持在85G一会后,内核会自动将其进程kill掉,增长的过程中没有被杀,如果不希望被杀可以执行
点击(此处)折叠或打开
pgrep -f “bigmem” | while read PID; do echo -17 > /proc/$PID/oom_adj;done
执行以上命令前后,明显会对比出效果,就可以体会到内核OOM机制的实际作用了。
注意:
1.Kernel-2.6.26之前版本的oomkiller算法不够精确,RHEL 6.x版本的2.6.32可以解决这个问题。
2.子进程会继承父进程的oom_adj。
3.OOM不适合于解决内存泄漏(Memory leak)的问题。
4.有时free查看还有充足的内存,但还是会触发OOM,是因为该进程可能占用了特殊的内存地址空间
程序运行了一段时间,有个进程挂掉了,正常情况下进程不会主动挂掉,简单分析后认为可能是运行时某段时间内存占用过大,系统内存不足导致触发了Linux操作系统OOM killer机制,将运行中的进程杀掉了。
一、Linux内核OOM killer机制
Linux 内核有个机制叫OOM killer(Out Of Memory killer),该机制会监控那些占用内存过大,尤其是瞬间占用内存很快的进程,然后防止内存耗尽而自动把该进程杀掉。内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码linux/mm/oom_kill.c,当系统内存不足的时候,out_of_memory()被触发,然后调用select_bad_process()选择一个”bad”进程杀掉。如何判断和选择一个”bad进程呢?linux选择”bad”进程是通过调用oom_badness(),挑选的算法和想法都很简单很朴实:最bad的那个进程就是那个最占用内存的进程。
0、什么时候触发?
内核在触发OOM机制时会调用到out_of_memory()函数,此函数的调用顺序如下:
__alloc_pages //内存分配时调用
|-->__alloc_pages_nodemask
|--> __alloc_pages_slowpath
|--> __alloc_pages_may_oom
| --> out_of_memory //触发 以上函数__alloc_pages_may_oom()在调用之前会先判断oom_killer_disabled的值,如果有值,则不会触发OOM机制;
布尔型变量oom_killer_disabled定义在文件mm/page_alloc.c中,并没有提供外部接口更改此值,但是在内核中此值默认为0,表示打开OOM-kill。
Linux中内存都是以页的形式管理的,所以不管是怎么申请内存,都会调用alloc_page()函数,最终调用到函数out_of_memory(),触发OOM机制。
【1】内核监测到系统内存不足时,该函数被触发执行:
/**
don’t have to be perfect here, we just have to be good.
*/
bool out_of_memory(struct oom_control *oc)
{
unsigned long freed = 0;
enum oom_constraint constraint = CONSTRAINT_NONE;
if (oom_killer_disabled)
return false;
if (!is_memcg_oom(oc)) {
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
if (freed > 0)
/* Got some memory back in the last second. */
return true;
}
/*
/*
/*
if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
current->mm && !oom_unkillable_task(current, NULL, oc->nodemask) &&
current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
get_task_struct(current);
oc->chosen = current;
oom_kill_process(oc, “Out of memory (oom_kill_allocating_task)”);
return true;
}
select_bad_process(oc); //选择一个“最坏的”进程杀掉。
/* Found nothing?!?! /
if (!oc->chosen) {
dump_header(oc, NULL);
pr_warn(“Out of memory and no killable processes…\n”);
/
* If we got here due to an actual allocation at the
* system level, we cannot survive this and will enter
* an endless loop in the allocator. Bail out now.
/
if (!is_sysrq_oom(oc) && !is_memcg_oom(oc))
panic(“System is deadlocked on memory\n”);
}
if (oc->chosen && oc->chosen != (void *)-1UL)
oom_kill_process(oc, !is_memcg_oom(oc) ? “Out of memory” :
“Memory cgroup out of memory”);
return !!oc->chosen;
}
【2】选择一个“最坏的”进程
/
‘points’. In case scan was aborted, oc->chosen is set to -1.
*/
static void select_bad_process(struct oom_control *oc)
{
if (is_memcg_oom(oc))
mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
else {
struct task_struct *p;
rcu_read_lock();
for_each_process(p)
if (oom_evaluate_task(p, oc))
break;
rcu_read_unlock(); }
oc->chosen_points = oc->chosen_points * 1000 / oc->totalpages;
}
【3】杀掉进程
static void oom_kill_process(struct oom_control *oc, const char *message)
{
struct task_struct *victim = oc->chosen;
struct mem_cgroup *oom_group;
static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);
/*
if (__ratelimit(&oom_rs))
dump_header(oc, victim);
/*
__oom_kill_process(victim, message);
/*
下面附加两个函数是select_bad_process()函数的实现细节,可不看。
static int oom_evaluate_task(struct task_struct *task, void *arg)
{
struct oom_control *oc = arg;
unsigned long points;
if (oom_unkillable_task(task, NULL, oc->nodemask))
goto next;
/*
* This task already has access to memory reserves and is being killed.
* Don't allow any other task to have access to the reserves unless
* the task has MMF_OOM_SKIP because chances that it would release
* any memory is quite low.
*/
if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
goto next;
goto abort;
}
/*
* If task is allocating a lot of memory and has been marked to be
* killed first if it triggers an oom, then select it.
*/
if (oom_task_origin(task)) {
points = ULONG_MAX;
goto select;
}
points = oom_badness(task, NULL, oc->nodemask, oc->totalpages);
if (!points || points < oc->chosen_points)
goto next;
/* Prefer thread group leaders for display purposes */
if (points == oc->chosen_points && thread_group_leader(oc->chosen))
goto next; select:
if (oc->chosen)
put_task_struct(oc->chosen);
get_task_struct(task);
oc->chosen = task;
oc->chosen_points = points; next:
return 0; abort:
if (oc->chosen)
put_task_struct(oc->chosen);
oc->chosen = (void *)-1UL;
return 1; }
/**
task consuming the most memory to avoid subsequent oom failures.
*/
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p = find_lock_task_mm(p);
if (!p)
return 0;
/*
/*
/* Normalize to oom_score_adj units */
adj *= totalpages / 1000;
points += adj;
/*
二、查看系统日志方法:
运行egrep -i -r ‘killed process’ /var/log命令,结果如下:
/var/log/syslog.1:May 6 10:02:51 iZuf66b59tpzdaxbchl3d4Z kernel: [1467990.340288] Killed process 17909 (procon) total-vm:5312000kB, anon-rss:4543100kB, file-rss:0kB
1
也可运行dmesg命令,结果如下:
[1471454.635492] Out of memory: Kill process 17907 (procon) score 143 or sacrifice child
[1471454.636345] Killed process 17907 (procon) total-vm:5617060kB, anon-rss:4848752kB, file-rss:0kB
1
2
显示可读时间的话可用dmesg -T查看:
[Wed May 15 14:03:08 2019] Out of memory: Kill process 83446 (machine) score 250 or sacrifice child
[Wed May 15 14:03:08 2019] Killed process 83446 (machine) total-vm:1920560kB, anon-rss:1177488kB, file-rss:1600kB
1
2
三、 附录
【1】附加__alloc_pages_nodemask()函数如下:
/*
This is the ‘heart’ of the zoned buddy allocator.
/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_mask; / The gfp_t that was actually used for allocation */
struct alloc_context ac = { };
/*
gfp_mask &= gfp_allowed_mask;
alloc_mask = gfp_mask;
if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
return NULL;
finalise_ac(gfp_mask, &ac);
/*
/* First allocation attempt */
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
/*
/*
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge(page, gfp_mask, order) != 0)) {
__free_pages(page, order);
page = NULL;
}
trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);
return page; } 最后,简单分析一下你的进程被Linux杀掉几个可能的原因:一种是内存泄露;一种是你的进程所需要的内存资源太大,系统无法满足,应该在设计时对进程需要的资源有个最大限制,不能让他无限增长;当然,也不一定全是你的问题,也有可能是同一主机的其他进程占用资源过多,但是Linux OOM选择“最坏“进程杀掉的算法是很简单粗暴的,就选中你的进程杀掉,也是有可能的。
Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。典型的情况是:某天一台机器突然ssh远程登录不了,但能ping通,说明不是网络的故障,原因是sshd进程被OOM killer杀掉了(多次遇到这样的假死状况)。重启机器后查看系统日志/var/log/messages会发现Out of Memory: Kill process 1865(sshd)类似的错误信息。
防止重要的系统进程触发(OOM)机制而被杀死:可以设置参数/proc/PID/oom_adj为-17,可临时关闭linux内核的OOM机制。内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以/proc/PID/oom_score中找到。我们运维过程中保护的一般是sshd和一些管理agent。
保护某个进程不被内核杀掉可以这样操作:
点击(此处)折叠或打开
echo -17 > /proc/$PID/oom_adj
如何防止sshd被杀,可以这样操作:
点击(此处)折叠或打开
pgrep -f “/usr/sbin/sshd” | while read PID;do echo -17 > /proc/$PID/oom_adj;done |
可以在计划任务里加入这样一条定时任务,就更安全了:
点击(此处)折叠或打开
#/etc/cron.d/oom_disable
/1*** root pgrep -f “/usr/sbin/sshd” | while read PID;do echo -17 > /proc/$PID/oom_adj;done |
为了避免重启失效,可以写入/etc/rc.d/rc.local
点击(此处)折叠或打开
echo -17 > /proc/$(pidof sshd)/oom_adj
至于为什么用-17而不用其他数值(默认值为0),这个是由linux内核定义的,查看内核源码可知:
以linux-3.3.6版本的kernel源码为例,路径为linux-3.6.6/include/linux/oom.h,阅读内核源码可知oom_adj的可调值为15到-16,其中15最大-16最小,-17为禁止使用OOM。oom_score为2的n次方计算出来的,其中n就是进程的oom_adj值,所以oom_score的分数越高就越会被内核优先杀掉。
当然还可以通过修改内核参数禁止OOM机制
点击(此处)折叠或打开
vm.panic_on_oom = 1 //1表示关闭,默认为0表示开启OOM
为了验证OOM机制的效果,我们不妨做个测试。
首先看看我系统现有内存大小,没错96G多,物理上还要比查看的值大一些。
再看看目前进程最大的有哪些,top查看,我目前只跑了两个java程序的进程,分别4.6G,再往后redis进程吃了21m,iscsi服务占了32m,gdm占了25m,其它的进程都是几M而已。
现在我自己用C写一个叫bigmem程序,我指定该程序分配内存85G
点击(此处)折叠或打开
#include
#include
#include
#define PAGE_SZ (1«12)
int main() {
int i;
int gb = 85; //以GB为单位分配内存大小
for (i = 0; i < ((unsigned long)gb<<30)/PAGE_SZ ; ++i) {
void *m = malloc(PAGE_SZ);
if (!m)
break;
memset(m, 0, 1);
}
printf("allocated %lu MB\n", ((unsigned long)i*PAGE_SZ)>>20);
getchar();
return 0; } 呵呵,效果明显,然后执行后再用top查看,排在第一位的是我的bigmem,RES是物理内存,已经吃满了85G。
继续观察,当bigmem稳定保持在85G一会后,内核会自动将其进程kill掉,增长的过程中没有被杀,如果不希望被杀可以执行
点击(此处)折叠或打开
pgrep -f “bigmem” | while read PID; do echo -17 > /proc/$PID/oom_adj;done |
执行以上命令前后,明显会对比出效果,就可以体会到内核OOM机制的实际作用了。
如果你觉得写C代码麻烦,我告诉大家另外一个最简单的测试触发OOM的方法,可以把某个进程的oom_adj设置到15(最大值),最容易触发。然后执行以下命令:
点击(此处)折叠或打开
echo f > /proc/sysrq-trigger // ‘f’ - Will call oom_kill to kill a memory hog process.
以下我来触发mysqld的OOM看看:
需要注意的是这个测试,只是模拟OOM,不会真正杀掉进程
点击(此处)折叠或打开
ps -ef | grep mysqld | grep -v grep
查看mysql进程,发现依然存在
注意:
1.Kernel-2.6.26之前版本的oomkiller算法不够精确,RHEL 6.x版本的2.6.32可以解决这个问题。
2.子进程会继承父进程的oom_adj。
3.OOM不适合于解决内存泄漏(Memory leak)的问题。
4.有时free查看还有充足的内存,但还是会触发OOM,是因为该进程可能占用了特殊的内存地址空间。
一.问题描述
最近对系统进行压测时发现Server日志报如下错误:java.lang.OutOfMemoryError: GC overhead limit exceeded。
查看Oracle OOM文档,在文档中找到以下内容:
Exception in thread thread_name: java.lang.OutOfMemoryError:GC Overhead limit exceeded
Cause: The detail message “GC overhead limitexceeded” indicates that the garbage collector is running all the time andJava program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown.This exception is typically thrown because the amount of live data barely fitsinto the Java heap having little free space for new allocations.
Action:Increase the heap size.The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with thecommand line flag -XX:-UseGCOverheadLimit.
原文链接如下:
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html
上面一段文字大概意思是JVM用98%的时间回收了不到2%的堆内存,因此预测到内存即将耗尽,抛出OOM异常来提示内存不足,相当于给用户一个警告信息。Oracle给的解决方法是提高堆内存,也可以用-XX:-UseGCOverheadLimit参数来关闭这个异常警告。
这种方式其实只是治标不治本,让用户提高堆内存容量,虽然解决了燃眉之急,但实际上并没有解决内存消耗过大的根本问题。如果堆内存一直增长,最终还会提示“GC overhead limit exceeded”。即使用XX:-UseGCOverheadLimit参数关闭OOM预测提示,当内存占用增长到极限,最后也会直接报 java.lang.OutOfMemoryError: Java heap space,造成进程停止的严重影响。所以Oracle给的解决方案并不靠谱,只能作为参考。
通过以上描述,可以推测进程中要么有任务一直在申请内存,要么发生了内存泄露。具体哪种原因还得对内存使用情况深入分析后判断,以下是分析的整个过程。
二.MAT分析
1.获取dump文件
登上服务器,用 Java自带的jmap生成dump 文件,命令如下:
jmap -dump:live,format=b,file= heap.hprof
从overview视图看到java.util.concurrent.ThreadPoolExecutor @ 0x744ce0aa 共占用了1.6G内存,比例占到了75%,可以初步判断有一个ThreadPoolExecutor的实例占据了大量内存,导致OOM。
3.Leak Suspects报告
点击overview视图下的Leak Suspects 按钮,查看分析结果。
从报告中看到QRCodeBatchTask类中引用的ThreadPoolExecutor的对象共占用了71.43%的内存,java.util.concurrent.LinkedBlockingQueue$Node 对象占用的最多。
图中Shallow Heap指对象自身占用内存大小,Retained Heap指对象自身加其引用对象总共占用内存大小。
从图中看出占用内存最多的是BufferedImages对象,QRCodeTask对象排到了第四,这个对象功能后面会提到。
选中BufferedImage右键查show object by classby incoming class,看到确实是QRCodeTask引用了BufferedImage。
选中BufferedImage右键查看Merge Shortest Paths to GC Roots ->exclude all phantim/weak/softetc. references,可以得知QRCodeTask中的的backGroundImage,logoImage变量占用内存最大。
至此,可以基本分析出是QRCodeTask类中的两个BufferImage 对象backGroundImage和logoImage占用了大量内存,大概17M。
MAT分析工作基本到此结束,下一步找到相应代码查看业务逻辑。
三.代码分析
1.当有请求过来时会执行以下函数,把QRCodeBatchTask添加到线程池中。
public void applyQRcode() {
exec.execute(new QRCodeBatchTask());
}
2.QRCodeBatchTask会不断往线程池提交QRCodeTask任务,并把生成的图片赋值给QRCodeTask。
public void run() {
backImage=ImageIO.read(backGroundImageFile);
logoImage=ImageIO.read(logoFile);
for (List list : lists) {
exec.execute(new QRCodeTask(backImage,logoImage));
)}}
3.QRCodeTask保存backGroundImage,logoImage对象,并在任务执行中用到。
public class QRCodeTask implements Runnable {
private BufferedImage backGroundImage, logoImage;
public QRCodeTask(String backGroundImage, logoImage ) {
this.backGroundImage = backImage;
this.logoImage = logoImage;
}
}
以上业务代码处理逻辑是每次请求过来时都会新建一个QRCodeBatchTask任务并提交到线程池中。QRCodeBatchTask会从磁盘读取两张图片,赋值给新建的QRCodeTask任务,所有新建的任务都会提交到线程池中去执行。
ThreadPoolExecutor原理是把提交的所有任务添加到LinkedBlockingQueue队列中,这也就是图中java.util.concurrent.LinkedBlockingQueue$Node对象引用QRCodeTask对象的原因。实际压测时候随着请求的持续增加,新建的QRCodeBatchTask越来越多,生成的图片也越来越多,最终导致占用的内存持续增长直至OOM发生。
四.问题解决
原因已经知道,解决问题就相对容易了。因为每个任务用到的backGroundImage,logoImage其实是一样的,因此可以用单例模式获取这两个图片,以此来保证进程内只保留一个图片对象,这样就减少了内存的占用。
通过重构代码,再次运行程序并用VisualVM进行监控可以观察到内存占用趋于平稳,没有再发生OOM,问题得到解决。