Android之内存优化与OOM

很久没写博客了,直接上干货:

Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。
Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基于寄存器的,而后者的指令集是基于堆栈的。
下图,读者自行替换
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

堆内存主要包含:

对象实例(Activity、Fragment、View等)
数组
字符串对象
Bitmap等大对象
静态变量

栈内存主要包含:

方法调用栈帧
局部变量
方法参数
返回地址
操作数栈

总结归类下:

在这里插入图片描述

垃 圾 回 收 算 法

垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的方式都有不同,所以不需要去了解算法的实现

复 制 算 法( (Copying) )

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使
用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
但是要注意:内存移动是必须实打实的移动(复制), 所 以 对 应 的 引用 用(直 直 接 指针 针)需 需 要 调 整。
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

在这里插入图片描述

Appel 式回收

一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1
和 Survivor2)
专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较
小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,
最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存
会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,
需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
在这里插入图片描述

标记- 清除算法(Mark-Sweep) )

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。
回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。
它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连
续内存而不得不提前触发另一次垃圾回收动作。
回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
在这里插入图片描述

标记- 整理算法(Mark-Compact) )

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端
边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。
我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用
对象的地方都需要更新( 直接指针需要调整)。
所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
在这里插入图片描述

App内存组成以及限制

Android给每个App分配一个VM,让App运行在dalvik上,这样即使App崩溃也不会影响到系统。系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash

由程序控制操作的内存空间在heap上,分java heapsizenative heapsize

  • Java申请的内存在vm heap上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常。

  • native层内存申请不受其限制,native层受native process对内存大小的限制

如何查看Android设备对App的内存限制

  1. 主要查看系统配置文件 build.prop,我们可以通过adb shell在 命令行窗口查看

adb shell cat /system/build.prop

在这里插入图片描述

在这里插入图片描述

  1. 通过代码获取
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m为单位
  1. 可以修改吗?
  • 修改 \frameworks\base\core\jni\AndroidRuntime.cpp
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool 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");//修改这里
     * }
  • 修改 platform/dalvik/+/eclair-release/vm/Init.c

    gDvm.heapSizeStart = 2 * 1024 * 1024;   // Spec says 16MB; too big for us.
    gDvm.heapSizeMax = 16 * 1024 * 1024;    // Spec says 75% physical mem
    

内存指标概念

Item 全称 含义 等价
USS Unique Set Size 物理内存 进程独占的内存
PSS Proportional Set Size 物理内存 PSS= USS+ 按比例包含共享库
RSS Resident Set Size 物理内存 RSS= USS+ 包含共享库
VSS Virtual Set Size 虚拟内存 VSS= RSS+ 未分配实际物理内存

总结:VSS >= RSS >= PSS >= USS,但/dev/kgsl-3d0部份必须考虑VSS

Android内存分配与回收机制

  • 内存分配

Android的Heap空间是一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当一个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。

在这里插入图片描述

1、Young Generation

由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当次Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。

2、Old Generation

一般情况下,年老代中的对象生命周期都比较长。

3、Permanent Generation

用于存放静态的类和方法,持久代对垃圾回收没有显著影响。

总结:内存对象的处理过程如下:

  • 1、对象创建后在Eden区。
  • 2、执行GC后,如果对象仍然存活,则复制到S0区。
  • 3、当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
  • 4、当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
  • 5、当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。

Java中:
Young Generation区的gc算法是 mimor GC

Old Generation 区的gc算法是 full GC --做内存回收时尽量别在这里做,因为速度慢很多,耗时,卡

Android内存分配:
Dalvik:
Linear Alloc: 匿名共享内存
Zygote Space: Zygote相关信息
Alloc Space : 每个进程 独占

ART:
Non Moving Space :不可移动的
Zygote Space :Zygote相关信息
Alloc Space :每个进程独占
Image Space : 预加载的类信息
Large Obj Space :大对象 bitmap

系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。

执行GC占用的时间与Generation和Generation中的对象数量有关:

  • Young Generation < Old Generation < Permanent Generation
  • Gener中的对象数量与执行时间成正比。

4、Young Generation GC

由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。

5、Old Generation GC

由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。

GC类型

在Android系统中,GC有三种类型:

  • kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
  • kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
  • kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。

可达性分析与GCRoots

在这里插入图片描述

Android低内存杀进程机制

Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:

在这里插入图片描述

  • Empty process(空进程)
  • Background process(后台进程)
  • Service process(服务进程)
  • Visible process(可见进程)
  • Foreground process(前台进程)

系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。

在这里插入图片描述

ActivityManagerService会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收( lowmemorykiller, Oom_killer)。这里只是大概的流程,中间过程还是很复杂的

什么是OOM

OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草

在这里插入图片描述

发生OOM的条件

  • Android 2.x系统 GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的时候就会发生OOM。 例如,假设有这么一段Dalvik输出的GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms,那么32586+8989+(新分配23975)=65550>64M时,就会发生OOM。
  • Android 4.x系统 Android 4.x的系统废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM

OOM原因分类

在这里插入图片描述

OOM代码分析

Android 虚拟机最终抛出OutOfMemoryError的地方

/art/runtime/thread.cc

void Thread::ThrowOutOfMemoryError(const char* msg) {
   
   
  LOG(WARNING) << StringPrintf("Throwing OutOfMemoryError \"%s\"%s",
      msg, (tls32_.throwing_OutOfMemoryError ? " (recursive case)" : ""));
  if (!tls32_.throwing_OutOfMemoryError) {
   
   
    tls32_.throwing_OutOfMemoryError = true;
    ThrowNewException("Ljava/lang/OutOfMemoryError;", msg);
    tls32_.throwing_OutOfMemoryError = false;
  } else {
   
   
    Dump(LOG_STREAM(WARNING));  // The pre-allocated OOME has no stack, so help out and log one.
    SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError());
  }
}

堆内存分配失败

/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()->GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
    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,"
      << " target footprint " << target_footprint_.load(std::memory_order_relaxed)
      << ", growth limit "
      << growth_limit_;
    
    //没有足够大小的连续地址空间
 // There is no fragmentation info to log for large-object space.
    if (allocator_type != kAllocatorTypeLOS) {
   
   
      CHECK(space != nullptr) << "allocator_type:" << allocator_type
                              << " byte_count:" << byte_count
                              << " total_bytes_free:" << total_bytes_free;
      space->LogFragmentationAllocFailure(oss, byte_count);
    }
  }    

创建线程失败

/art/runtime/thread.cc

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
   
   
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();

 // TODO: remove from thread group?
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  {
   
   
    std::string msg(child_jni_env_ext.get() == nullptr ?
        StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
        StringPrintf("pthread_create (%s stack) failed: %s",
                                 PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }

Android 内存分析命令介绍

常用的内存调优分析命令:

  1. dumpsys meminfo
  2. procrank
  3. cat /proc/meminfo
  4. free
  5. showmap
  6. vmstat

dumpsys meminfo

在这里插入图片描述

相关参数的说明:

Pss Total:是一个进程实际使用的内存,该统计方法包括比例分配共享库占用的内存,即如果有三个进程共享了一个共享库,则平摊分配该共享库占用的内存。Pss Total统计方法的一个需要注意的地方是如果使用共享库的一个进程被杀死,则共享库的内存占用按比例分配到其他共享该库的进程中,而不是将内存资源返回给系统,这种情况下PssTotal不能够准确代表内存返回给系统的情况。

Private Dirty:进程私有的脏页内存大小,该统计方法只包括进程私有的被修改的内存。

Private Clear:进程私有的干净页内存大小,该统计方法只包括进程私有的没有被修改的内存。

Swapped Dirty:被交换的脏页内存大小,该内存与其他进程共享。

其中private Dirty + private Clean = Uss,该值是一个进程的使用的私有内存大小,即这些内存唯一被该进程所有。该统计方法真正描述了运行一个进程需要的内存和杀死一个进程释放的内存情况,是怀疑内存泄露最好的统计方法。

共享比例:sharing_proportion = (Pss Total - private_clean - private_dirty) / (shared_clean + shared_dirty)

能够被共享的内存:swappable_pss = (sharing_proportion * shared_clean) + private_clean

Native Heap:本地堆使用的内存,包括C/C++在堆上分配的内存

Dalvik Heap:dalvik虚拟机使用的内存

Dalvik other:除Dalvik和Native之外分配的内存,包括C/C++分配的非堆内存

Cursor:数据库游标文件占用的内存

Ashmem:匿名共享内存

Stack:Dalvik栈占用的内存

Other dev:其他的dev占用的内存

.so mmap:so库占用的内存

.jar mmap:.jar文件占用的内存

.apk mmap:.apk文件占用的内存

.ttf mmap:.ttf文件占用的内存

.dex mmap:.dex文件占用的内存

image mmap:图像文件占用的内存

code mmap:代码文件占用的内存

Other mmap:其他文件占用的内存

Graphics:GPU使用图像时使用的内存

GL:GPU使用GL绘制时使用的内存

Memtrack:GPU使用多媒体、照相机时使用的内存

Unknown:不知道的内存消耗

Heap Size:堆的总内存大小

Heap Alloc:堆分配的内存大小

Heap Free:堆待分配的内存大小

Native Heap | Heap Size : 从mallinfo usmblks获的,当前进程Native堆的最大总共分配内存

Native Heap | Heap Alloc : 从mallinfo uorblks获的,当前进程navtive堆的总共分配内存

Native Heap | Heap Free : 从mallinfo fordblks获的,当前进程Native堆的剩余内存

Native Heap Size ≈ Native Heap Alloc + Native Heap Free

mallinfo是一个C库,mallinfo()函数提供了各种各样通过malloc()函数分配的内存的统计信息。

Dalvik Heap | Heap Size : 从Runtime totalMemory()获得,Dalvik Heap总共的内存大小

Dalvik Heap | Heap Alloc : 从Runtime totalMemory() - freeMemory()获得,Dalvik Heap分配的内存大小

Dalvik Heap | Heap Free : 从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小

Dalvik Heap Size = Dalvik Heap Alloc + Dalvik Heap Free

Obejcts当前进程中的对象个数

Views:当前进程中实例化的视图View对象数量

ViewRootImpl:当前进程中实例化的视图根ViewRootImpl对象数量

AppContexts:当前进程中实例化的应用上下文ContextImpl对象数量

Activities:当前进程中实例化的Activity对象数量

Assets:当前进程的全局资产数量

AssetManagers:当前进程的全局资产管理数量

Local Binders:当前进程有效的本地binder对象数量

Proxy Binders:当前进程中引用的远程binder对象数量

Death Recipients:当前进程到binder的无效链接数量

OpenSSL Sockets:安全套接字对象数量

SQL

MEMORY_USED:当前进程中数据库使用的内存数量,kb

PAGECACH

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值