一、OOM介绍
1、VM运行时内存分析
JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。JVM包括下列几个运行时数据区域:
1.程序计数器(Program Counter Register):
此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。2.Java虚拟机栈(Java Virtual Machine Stacks):
在VM Spec中对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。3.本地方法栈(Native Method Stacks):
和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。4.Java堆(Java Heap)
对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例(以及数组),绝大部分的对象实例都在这里分配。
Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等。
根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。5.方法区(Method Area)
- 6.运行时常量池(Runtime Constant Pool):
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。
2、OOM出现原因
- 已知前提:
Android的应用程序所能申请的最大内存都是有限的 - 定义:
OOM是指APP向系统申请内存的请求超过了应用所能有的最大阀值的内存,系统无法再分配多余的空间,就会造成OOM - 出现原因:
1、持续发生了内存泄漏(Memory Leak),累积到一定程度导致OOM
持续发生了内存泄漏(Memory Leak),累积到一定程度导致OOM;
2、一次性申请很多内存(比如说一次创建大的数组或者是载入大的文件如图片的时候) - 造成结果:
发生oom错误,进程被强制kill掉,kill掉的进程内存会被系统回收
二、StrictMode介绍
StrictMode:严苛模式,主要用来检测程序中几种违例情况的Android原生开发者工具。 主要分为ThreadPolicy(线程检测策略)跟VmPolicy(虚拟机检测策略)两类检测内容:
1、ThreadPolicy
- detectDiskReads()--主线程读取文件
- detectDiskWrites()--主线程写文件
- detectNetwork()--主线程网络操作
- detectCustomSlowCalls()--自定义耗时操作
2、VmPolicy
- detectLeakedSqlLiteObjects--Sqlite对象泄漏
- detectActivityLeaks--Activity泄漏
- detectLeakedClosableObjects--未关闭的Closable对象泄露
- detectLeakedRegistrationObjects--广播注册后没取消注册
3、集成方式
可以按照不同场景需求选择检测不同违规操作。推荐在Application onCreate()方法中集成:
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()//线程策略(ThreadPolicy)
.detectDiskReads()//检测在UI线程读磁盘操作
.detectDiskWrites()//检测UI线程写磁盘操作
.detectCustomSlowCalls()//发现UI线程调用的哪些方法执行得比较慢
.detectResourceMismatches()//最低版本为API23 发现资源不匹配
.detectNetwork() //检测在UI线程执行网络操作
.penaltyDialog()//一旦检测到弹出Dialog
.penaltyDropBox()//一旦检测到将信息存到DropBox文件夹中 data/system/dropbox
.penaltyLog()//一旦检测到将信息以LogCat的形式打印出来
.permitDiskReads()//允许UI线程在磁盘上读操作
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()//虚拟机策略(VmPolicy)
.detectActivityLeaks()//最低版本API11 用户检查 Activity 的内存泄露情况
.detectCleartextNetwork()//最低版本为API23 检测明文的网络
.detectFileUriExposure()//最低版本为API18 检测file://或者是content://
.detectLeakedClosableObjects()//最低版本API11 资源没有正确关闭时触发
.detectLeakedRegistrationObjects()//最低版本API16 BroadcastReceiver、ServiceConnection是否被释放
.detectLeakedSqlLiteObjects()//最低版本API9 资源没有正确关闭时回触发
.setClassInstanceLimit(TestLeakActivity.class, 2)//设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
.penaltyLog()
.build());
}
4、集成StrictMode后代码违规表现
由上面StrictMode API可知,检测到违规代码时,我们可以选择以应用崩溃/弹框/日志输出等形式表现出来。一般推荐输出到日志即可。下面简单列举下执行到代码中违规操作时日志表现:
- I/O流未关闭
2019-05-06 19:51:25.523 22941-22951/com.whh.strictmode E/StrictMode: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
java.lang.Throwable: Explicit termination method 'close' not called
at dalvik.system.CloseGuard.open(CloseGuard.java:180)
at java.io.FileOutputStream.(FileOutputStream.java:222)
at java.io.FileOutputStream.(FileOutputStream.java:169)
at com.whh.strictmode.MainActivity.testReadWrite(MainActivity.java:83)
at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)
- Activity泄漏
2019-05-06 19:52:48.467 23148-23148/com.whh.strictmode E/WindowManager: android.view.WindowLeaked: Activity com.whh.strictmode.leakactivity.TestLeakActivity has leaked window DecorView@86a60b[] that was originally added here
at android.view.ViewRootImpl.(ViewRootImpl.java:418)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:331)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
at android.app.Dialog.show(Dialog.java:322)
at com.whh.strictmode.leakactivity.TestLeakActivity.onCreate(TestLeakActivity.java:38)
2019-05-06 19:53:18.916 23148-23148/com.whh.strictmode E/StrictMode: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
- Bitmap泄漏
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 51916812 byte allocation with 4188384 free bytes and 24MB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:620)
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:660)
at com.whh.strictmode.MainActivity.testBitmap(MainActivity.java:105)
at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)
三、如何避免OOM
1、I/O流用完及时关闭
推荐用try-with-resource语句--更优雅的关闭资源。
(JDK7及其之后的资源关闭方式)
try-with-resource并不是JVM虚拟机的新增功能,只是JDK实现了一个语法糖,实际实现原理还是try-catch-finally。
以前用try-catch-finally使用I/O流代码如下:
//try-catch外部定义I/O流
InputStream is = null;
OutputStream os = null;
try {
//try括号中I/O赋值
is = new FileInputStream(src);
os = new FileOutputStream(dst);
//...进行一些I/O操作
} catch (Exception e) {
} finally {
//finally中关闭I/O流
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
}
}
这种方式代码繁杂不说,有时候还容易忘记关闭输入输出流。而换成try-with-resource语句后,代码如下:
try (InputStream is = new FileInputStream(src);
OutputStream os = new FileOutputStream(dst)) {
//...进行一些I/O操作
} catch (Exception e) {
}
这种方式优雅简洁,而且不用担心忘记关闭输入输出流,因为try()括号中的I/O流会自动关闭
2、确保Activity实例引用不会被其他类长期持有
- 1、Activity中非静态内部类会持有Activity实例引用:Java的非静态内部类在构造的时候,会将外部类的引用传递进来,并且作为内部类的一个属性,因此,内部类会隐式地持有其外部类的引用。
所以,定义内部类时一定要注意看当前类是否持有Activity引用或者当前类是否是Activity。 - 2、将Activity当成Context传入其他类,会导致其他类持有Activity实例引用。
所以,一般情况下要用Context时最好用ApplicationContext,必须用ActivityContext时要注意Context引用回收。 - 3、将Activity中View注入其他类时,由于View持有ActivityContext引用,也会导致其他类持有Activity实例引用。
所以,将View注入其他类时,确保该View引用在用完后被回收。 - 4、Activity实现了某些接口,作为观察者被注册到其他类时,也会导致Activity引用被其他类持有。
所以,Activity中注册监听与反注册最好成对出现。 - 5、Activity生命周期内注册广播时,要在对应生命周期中取消广播注册。
3、图片加载
- 1、尽量使用图片加载框架(ImageLoader/Picasso/Glide...)加载图片,因为使用图片加载框架不需要去考虑图片Bitmap回收问题等,而且0图片加载框架基本都支持扩展(比如图片转换等)跟自定义(比如图片缓存大小、位置等)。
- 2、不用框架加载图片时,请用LruCache管理图片缓存列表。
- 3、如果以上两种方式都不用,一定要确保Bitmap使用完后合理回收。
- 4、加载图片时,一定要考虑到图片尺寸压缩/颜色质量,根据需要保证图片缓存的合理大小跟颜色质量。由于Android加载图片基本使用BitmapFactory加载,这里简单介绍下两种图片利用技巧:
//1、利用inSampleSize进行图片采样
BitmapFactory.Options options = new BitmapFactory.Options();
//inSampleSize越大,压缩率越大。inSampleSize==1时表示原图
//当确定图片大小跟需要的图片大小时,可以计算出采样率进行图片压缩
options.inSampleSize = 2;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//2、利用inSampleSize进行图片采样
BitmapFactory.Options options = new BitmapFactory.Options();
//当对图片透明度需求不高时,可以将图片颜色质量设为RGB_565
options.inPreferredConfig = Config.RGB_565;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//3、利用inBitmap复用旧的Bitmap的内存,而不用重新分配以及销毁旧Bitmap,进而改善运行效率
BitmapFactory.Options options = new BitmapFactory.Options();
//mOldBitmap是一个旧的Bitmap,必须是mutable的(支持修改).新的bmp直接复用该mOldBitmap的内存空间
//使用条件是mOldBitmap占用的空间必须大于等于新bmp占用的内存空间
options.inBitmap = mOldBitmap;
Bitmap bmp = BitmapFactory.decodeFile(path, options);