Android OOM问题及优化总结

业精于勤荒于嬉,写文章练习表达能力,写代码练习基本工。

OOM和内存优化总结

什么是OOM?

OOM 即 (java.lang.OutOfMemoryError), JVM没有足够内存给对象分配空间,超过jvm的堆空间最大值(-Xmx参数),此异常就会被触发,导致应用强制被杀死。

OOM原因?

对于java程序员来说,我们一般只管创建对象,而对象的回收,我们很少操心,是因为JVM有垃圾回收器来定期执行GC,负责回收已经引用不到的无用对象来释放内存。不过,我们的应用还是出现内存泄露或者内存溢出,这也是导致oom的两大因素。

内存泄露和内存溢出?

  • 内存泄露,是指之前分配的内存空间,不再使用,但也无法被垃圾回收器释放,这种情况如果一直堆积,会导致内存溢出。
  • 内存溢出,没有足够的内存来分配空间了,空间不够了。

OOM发生在哪个区域?

我们先来认识一些jvm内存模型:

在这里插入图片描述

按照JVM的规范,除了程序计数器区,其他区域都可能出现OOM。

jvm 内存分配与回收机制

我们平时写java代码,绝大部分的OOM都发生在堆区,因此着重了解一下堆空间对象的内存模型和回收机制。

在这里插入图片描述

  • 堆空间划分为,新生代(eden + S0 + S1), 老年代(Tenured)和永久代(Permanent),永久代java1.8已经取消,改为元空间。
  • 新创建的对象基本都分配在新生代eden区,大对象直接分配到老年代。
  • 一次GC后,eden区仍然存活的对象被移到S0;S0内存活的对象移到S1;S1内存活的对象被移到Tenured
  • 每一次GC,对象年龄增长一岁,到达15岁(默认,可设置),晋升到老年代。

对象被回收的判断算法

  • 可达性分析 :以 GC Root 为分析的起点 , 查找对象的引用 , 如果找到一个对象 , 无法被 GC Root 直接或间接引用到 , 那么该对象就可以被回收了 。

在这里插入图片描述

  • 引用计数法 :给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。

垃圾回收机制(GC)

垃圾回收算法

  • 标记-清除
  • 标记-整理
  • 复制

垃圾回收主要在新生代和老年代工作,在分代收集模型中(上图),不同的分代采用不同的收集算法:

  • eden和Survivor:通常是复制算法
  • Tenured:通常是标记整理算法,android是CMS

Android App 的内存限制

在这里插入图片描述

  • Java Heap :Java申请的内存,受 vm.heapsize 大小限制。

  • native Heap : c/c++层申请的内存,不受 vm.heapsize 大小限制。

如何修改:

代码位置 /frameworks/base/core/jni/AndroidRuntime.cpp

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)
{

/*
     * The default starting and maximum size of the heap.  Larger
     * values should be specified in a product property override.
     */
    parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
    parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");

    parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");
  
}

Android 抛出OOM的源头

堆分配失败

代码位置:/art/runtime/gc/heap.cc

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
  // If we're in a stack overflow, do not create a new exception. It would require running the
  // constructor, which will of course still be in a stack overflow.
  if (self->IsHandlingStackOverflow()) {
    self->SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError());
    return;
  }

  std::ostringstream oss;
  size_t total_bytes_free = GetFreeMemory();
  oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
  // If the allocation failed due to fragmentation, print out the largest continuous allocation.
  if (total_bytes_free >= byte_count) {
    space::AllocSpace* space = nullptr;
    if (allocator_type == kAllocatorTypeNonMoving) {
      space = non_moving_space_;
    } else if (allocator_type == kAllocatorTypeRosAlloc ||
               allocator_type == kAllocatorTypeDlMalloc) {
      space = main_space_;
    } else if (allocator_type == kAllocatorTypeBumpPointer ||
               allocator_type == kAllocatorTypeTLAB) {
      space = bump_pointer_space_;
    } else if (allocator_type == kAllocatorTypeRegion ||
               allocator_type == kAllocatorTypeRegionTLAB) {
      space = region_space_;
    }
    if (space != nullptr) {
      space->LogFragmentationAllocFailure(oss, byte_count);
    }
  }
  self->ThrowOutOfMemoryError(oss.str().c_str());
}

常用的内存分析命令

adb shell dumpsys meminfo [package name]

在这里插入图片描述

adb shell procrank

内存占用排行榜,没试成功,需要自己上传sh。

adb shell cat /proc/meminfo

在这里插入图片描述

adb shell free

在这里插入图片描述

total = used + free

adb shell top

在这里插入图片描述


内存泄露分析工具

Memory Analyzer Tool

下载地址

1,抓取内存申请快照hprof文件

adb shell am dumpheap com.xxx.xxx /data/local/tmp/14_54.hprof

2,转换为标准hprof

hprof-conv 14_54.hprof 14_54_R.hprof

hprof-conv在sdk的platform-tools目录中

3,使用mat打开

在这里插入图片描述

首页会有当前内存泄露的猜想,大对象,数量多的对象会被置为怀疑目标。

分析内存泄露

大对象分析

点击 Overview - Dominator ,对象按照占用空间大小排序

在这里插入图片描述

重点排查排在上面的大对象,下图中,很明显竟然有多个相同activity,且保持引用了大对象,无法释放,造成内存泄露。
在这里插入图片描述

GC Root 引用链查看

上图中,选择一项,右键 -> Paths to GC Roots–>exclude all phantom/weak/soft etc reference ,重点分析强引用。

在这里插入图片描述


可以看到context引用链,定位到自己的代码里面,本例是一个类静态持有了Activity的context,Activity销毁后,context无法释放造成内存泄露。

直方图比较

如果问题是内存随着时间缓慢增长,单个hprof看不出来,那么就隔一段时间再去一次快照,对两个快照文件进行比较,看那个对象一直在增长。

在这里插入图片描述

之前分析问题的hprof找不到了,临时找了两个,上面这个图片仅供步骤参考,具体以实际操作为准。


Android Profile

打开Profile页面,选择监控的程序,点击MEMORY–>dump按钮。

在这里插入图片描述

同样在android studio上可以查看hprof进行分析。

在这里插入图片描述


LeakCanary

github地址

引用

dependencies {
    
    
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
}

调用

public class App extends Application {
    
    

    @Override
    public void onCreate() {
    
    
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
    
    
            return;
        }
        LeakCanary.install(this);
    }
}

例子

public class MainActivity extends AppCompatActivity {
    
    

    private static int k = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(runnable).start();
    }


    public Runnable runnable = new Runnable() {
    
    
        @Override
        public void run() {
    
    
            while (true){
    
    
                MainActivity.k++;
                Log.d("TAG",""+k);
            }

        }
    };

}

发生内存泄露,进入这个页面直接查看。
在这里插入图片描述
这个图片尺寸怎么缩小?哭


内存泄露,内存溢出常见场景和解决方案总结

以下摘抄自他人总结的,在这里做备份。

1、资源未关闭

对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap
等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

2、注册未注销

例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象

尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4、单例造成的内存泄漏

优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封
装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

5、非静态内部类的静态实例

该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源
不能正常回收。如果需要使用Context,尽量使用Application Context。

6、Handler临时性内存泄漏

Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引
用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,
则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,
当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message
持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回
收,引发内存泄漏。解决方案如下所示:

​ 1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这
样在回收时,也可以回收Handler持有的对象。
​ 2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中
有待处理的消息需要处理。

需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于
类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静
态内部类。

7、容器中的对象没清理造成的内存泄漏

在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

8、WebView

WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为
WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业
务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用ListView时造成的内存泄漏

在构造Adapter时,使用缓存的convertView。

如有错误,请帮忙指出。

参考

Android内存优化之OOM

用户指南 - android app性能优化大汇总(内存性能优化)

使用内存性能分析器查看应用的内存使用情况

【Android 内存优化】内存抖动 ( 垃圾回收算法总结 | 分代收集算法补充 | 内存抖动排查 | 内存抖动操作 | 集合选择 )

JVM内存管理:深入Java内存区域与OOM

猜你喜欢

转载自blog.csdn.net/lucky_tom/article/details/113248541