精华内容
下载资源
问答
  • 2022-02-24 17:33:43
    转载请注明出处:https://blog.csdn.net/mythmayor/article/details/123117007

    前言

    在Android或Java中,我们一般不用担心内存管理,这是因为Java虚拟机(JVM:Java Virtual Machine)存在垃圾回收机制(GC:Garbage Collection),垃圾回收器会对内存进行管理。相比于其它语言(例如C语言),会要求主动释放申请的内存,所以在编程的时候需要考虑内存申请和内存释放的时机。Java GC的存在从一定程度上减少了我们的工作量,但带来的后果就是很多时候我们会滥用内存,比如说申请了一块较大的内存,很少会去关注这块内存何时释放以及如何释放,从而导致应用占用的内存越来越大,甚至到最后产生了内存溢出(OOM:Out Of Memory),进而引发程序崩溃。所以,关注内存变化并了解一些内存优化和监控是有必要的。

    一、内存概述

    1.JVM内存结构

    在这里插入图片描述

    上图描述了HelloWorld.java文件被JVM加载到内存中的过程:首先需要经过编译器编译,生成HelloWorld.class 字节码文件,然后通过ClassLoader(类加载器)将HelloWorld.class加载到JVM的内存中。JVM中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

    程序计数器(Program Counter Register)

    Java程序是多线程的,CPU可以在多个线程中分配执行时间片段。当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,方便CPU重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。

    程序计数器是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。

    虚拟机栈

    JVM是基于栈的解释器执行的,DVM是基于寄存器解释器执行的。

    这里说到的栈就是虚拟机栈。

    虚拟机栈是线程私有的,与线程的生命周期同步。在Java虚拟机规范中,对这个区域规定了两种异常情况:

    • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
    • OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出。

    虚拟机栈的初衷是用来描述Java方法执行的内存模型,每个方法被执行的时候,JVM都会在虚拟机栈中创建一个栈帧(栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等)。

    本地方法栈

    本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及JNI可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

    Java堆(Heap)是JVM所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是Java垃圾收集器(GC)管理的主要区域,有时候也叫作“GC堆”。同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

    方法区

    方法区(MethodArea)也是JVM规范里规定的一块运行时数据区。方法区主要是存储已经被JVM加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

    2.Android中内存分配

    每一个Android设备都会有不同的RAM总大小与可用空间,因此不同设备为App提供了不同大小的内存限制(这里的内存主要指的是堆内存)。如果你觉得分配的内存不够用,还可以通过在清单文件中开启android:largeHeap="true"来获取更多内存。然而,能够获取更大内存的设计本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用),所以不要轻易地因为需要使用大量内存而去申请largeHeap,只有当你清楚的知道哪里会使用大量的内存并且为什么这些内存必须被保留时再去使用largeHeap。因为使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。在切换任务时,系统的性能会大打折扣。另外, largeHeap并不一定能够获取到更大的内存。在某些有严格限制的机器上,largeHeap的大小和通常的heapSize是一样的。因此即使你申请了largeHeap,你还是应该通过执行getMemoryClass()来检查实际获取到的内存大小。

    获取应用内存大小:

    ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    //获取正常情况下内存的大小
    int memoryClass = manager.getMemoryClass();
    //获取开启largeHeap最大的内存大小
    int largeMemoryClass = manager.getLargeMemoryClass();
    

    二、内存优化方案

    我们在做内存优化时,无非有两个方向:

    • 内存溢出(OOM)
    • 内存泄漏(Memory Leak)

    1.内存溢出(OOM)

    通常在排查内存溢出相关的问题时,可以从以下几个方面入手:

    • 加载了大量的Bitmap
    • 创建了过多的对象
    • 加载过大的对象
    • 加载的资源过多,来不及释放
    • 内存泄漏

    总之,内存溢出的产生都是由于App申请的内存超过了应用的内存上限,这是JVM就会抛出OutOfMemoryError,从而导致应用程序的崩溃。解决内存溢出的方法主要有:(1)使用LruCache对图片进行缓存;(2)避免在for循环中创建对象;(3)加载大图片时使用图片压缩、图片裁剪等技术;(4)及时释放不再使用的对象占用的内存;(5)不要将Context和View存储在静态变量中。(6)使用内存监控机制,例如在Application或Activity中监听onTrimMemory(int level)方法的调用,根据不同的内存状态释放无用资源或者清除图片缓存等;(7)内存泄漏同样是内存溢出的关键因素之一,可针对内存泄漏问题进行分析并处理。

    2.内存泄漏(Memory Leak)

    通常在排查内存泄漏相关的问题时,可以从以下几个方面入手:

    • 注册后未取消注册造成的内存泄漏(例如:广播)
    • 静态变量持有Activity的引用
    • 单例模式持有Activity的引用
    • 查询数据库后没有关闭游标Cursor
    • 构造Adapter时,没有使用convertView重用
    • Bitmap对象不再使用时未调用recycle()释放内存
    • 对象被生命周期长的对象引用(例如:Activity被静态集合引用导致Activity不能释放)
    • Handler造成的内存泄漏
    • 非静态的内部类中持有外部类的引用
    • 匿名内部类/非静态内部类和异步线程

    使用到的工具有:LeakCanary、Profiler等。

    三、低内存监控方案

    Android中提供了两个低内存监控方法:onLowMemory()与onTrimMemory(int level)。可以通过这两个方法判断当前是否是低内存的状态,并且根据不同的情况释放自身内存,以避免应用程序被系统杀掉,提高应用程序的用户体验。

    1.onLowMemory()

    /**
      * This is called when the overall system is running low on memory, and
      * actively running processes should trim their memory usage.  While
      * the exact point at which this will be called is not defined, generally
      * it will happen when all background process have been killed.
      * That is, before reaching the point of killing processes hosting
      * service and foreground UI that we would like to avoid killing.
      *
      * <p>You should implement this method to release
      * any caches or other unnecessary resources you may be holding on to.
      * The system will perform a garbage collection for you after returning from this method.
      * <p>Preferably, you should implement {@link ComponentCallbacks2#onTrimMemory} from
      * {@link ComponentCallbacks2} to incrementally unload your resources based on various
      * levels of memory demands.  That API is available for API level 14 and higher, so you should
      * only use this {@link #onLowMemory} method as a fallback for older versions, which can be
      * treated the same as {@link ComponentCallbacks2#onTrimMemory} with the {@link
      * ComponentCallbacks2#TRIM_MEMORY_COMPLETE} level.</p>
      */
    void onLowMemory();
    

    Google对该方法的说明:

    当整个系统的内存不足时调用它,并且主动运行的进程应该减少它们的内存使用量。虽然没有定义调用它的确切时间,但通常会在所有后台进程都被杀死时发生。也就是说,在达到终止托管服务和前台 UI 的进程之前,我们希望避免程序终止。

    您应该实现此方法以释放您可能持有的任何缓存或其他不必要的资源。从这个方法返回后,系统会为你执行一次垃圾回收。

    最好,您应该根据不同级别的内存需求实现ComponentCallbacks2#onTrimMemory从 增量卸载您的资源。ComponentCallbacks2该 API 可用于 API 级别 14 及更高级别,因此您应该只将此onLowMemory()方法用作旧版本的后备,可以将其视为ComponentCallbacks2#onTrimMemoryComponentCallbacks2.TRIM_MEMORY_COMPLETE级别相同。

    onLowMemory()方法定义在ComponentCallbacks接口中,实现该接口的有Application、Activity、Fragment、Service、ContentProvider。当整个系统运行内存不足时,就会被调用。此时为了避免程序崩溃,可以采取一些措施,例如在Application中清除缓存,在Activity和Fragment中释放无用资源(例如Bitmap、数组、控件资源等)等。

    根据Google官方的说法,该方法是在后台进程都被杀死时调用,此时面临的局面是下一步很有可能会将当前的前台进程杀死,但是该方法没有确切的调用时间,只是用于Android旧版本低内存监控。Android4.0以后可以直接用onTrimMemory(int level)方法来监听内存状态,并且onLowMemory()调用时可以理解为达到了onTrimMemory(int level)方法的ComponentCallbacks2.TRIM_MEMORY_COMPLETE级别,即最高级别。

    在实际测试中并未监测到onLowMemory()方法的调用,只监测到onTrimMemory(int level)方法的调用,并且onTrimMemory(int level)方法的level达到最高级别时也未调用onLowMemory()方法。

    2.onTrimMemory(int level)

    /**
      * Called when the operating system has determined that it is a good
      * time for a process to trim unneeded memory from its process.  This will
      * happen for example when it goes in the background and there is not enough
      * memory to keep as many background processes running as desired.  You
      * should never compare to exact values of the level, since new intermediate
      * values may be added -- you will typically want to compare if the value
      * is greater or equal to a level you are interested in.
      *
      * <p>To retrieve the processes current trim level at any point, you can
      * use {@link android.app.ActivityManager#getMyMemoryState
      * ActivityManager.getMyMemoryState(RunningAppProcessInfo)}.
      *
      * @param level The context of the trim, giving a hint of the amount of
      * trimming the application may like to perform.
      */
    void onTrimMemory(@TrimMemoryLevel int level);
    

    Google对该方法的说明:

    当操作系统确定现在是从其进程中减少不需要的内存的好时机时调用。例如,当它进入后台并且没有足够的内存来保持尽可能多的后台进程运行时,就会发生这种情况。您永远不应与级别的确切值进行比较,因为可能会添加新的中间值——您通常希望比较该值是否大于或等于您感兴趣的级别。

    要在任何时候检索进程当前的修剪级别,您可以使用ActivityManager.getMyMemoryState(RunningAppProcessInfo);

    onTrimMemory(int level)是Android4.0提供的API(Android4.0以下的版本中使用onLowMemory()方法),方法定义在ComponentCallbacks2接口中,实现该接口的有Application、Activity、Fragment、Service、ContentProvider。该方法和onLowMemory()方法类似,当整个系统运行内存不足时,就会被调用,不过onTrimMemory(int level)方法中多了当前内存级别(水平)情况,系统会根据不同的内存状态,来响应不同的内存释放策略,使用起来更加灵活,场景也更加丰富。

    onTrimMemory(int level)方法的内存级别:

    • TRIM_MEMORY_RUNNING_MODERATE(5)

      应用程序正在运行,并且不会被杀死,但设备已经处于低内存状态,并且开始杀死LRU缓存里的内存。

      内存不足(后台进程超过5个),并且当前进程优先级比较高,需要清理内存。

    • TRIM_MEMORY_RUNNING_LOW(10)

      应用程序正在运行,并且不会被杀死,但设备处于内存更低的状态,所以你应该释放无用资源以提高系统性能(直接影响App性能)。

      内存不足(后台进程不足5个),并且当前进程优先级比较高,需要清理内存。

    • TRIM_MEMORY_RUNNING_CRITICAL(15)

      应用程序还在运行,但系统已经杀死了LRU缓存里的大多数进程,所以你应该在此时释放所有非关键的资源。如果系统无法回收足够的内存,它会清理掉所有LRU缓存,并且开始杀死之前优先保持的进程,像那些运行着Service的。

      内存不足(后台进程不足3个),并且当前进程优先级比较高,需要清理内存。

    • TRIM_MEMORY_UI_HIDDEN(20)

      当应用程序中所有的UI组件全部不可见的时候会触发,这和onStop方法的区别是:onStop方法只是当一个Activity完全不可见的时候就会调用,此时可以释放一些Activity的资源,比如取消网络连接或注销广播接收器等,但是UI相关的资源要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)回调之后再去释放。

      内存不足,并且该进程的UI已经不可见了。

    • TRIM_MEMORY_BACKGROUND(40)

      系统运行在低内存状态,并且你的进程已经接近LRU列表的顶端(即将被清理)。虽然你的App进程还没有很高的被杀死风险,系统可能已经清理LRU里的进程,你应该释放那些容易被恢复的资源,如此可以让你的进程留在缓存里,并且当用户回到App时快速恢复。

      内存不足,并且当前进程是后台进程。

    • TRIM_MEMORY_MODERATE(60)

      系统运行在低内存状态,你的进程在LRU列表中间附近。如果系统变得内存紧张,可能会导致你的进程被杀死。

      内存不足,并且当前进程在后台进程列表的中部。

    • TRIM_MEMORY_COMPLETE(80)

      系统运行在低内存状态,如果系统没有恢复内存,你的进程是首先被杀死的进程之一。你应该释放所有不重要的资源来恢复你的App状态。

      内存不足,并且当前进程在后台进程列表最后一个,马上就要被清理。

    3.onLowMemory()与onTrimMemory(int level)触发时机总结

    (1)onLowMemory()被回调时,已经没有后台进程;而onTrimMemory(int level)被回调时,还有后台进程。
    (2)onLowMemory()是在最后一个后台进程被杀时调用,一般情况是LowMemoryKiller杀进程后触发;而onTrimMemory(int level)的触发更频繁,每次计算进程优先级时,只要满足条件,都会触发。

    (3)通过一键清理后,onLowMemory()不会被触发,而onTrimMemory(int level)会被触发一次。

    (4)在Application、 Activity、Fragement、Service、ContentProvider中都可以重写回调方法,对onLowMemory()/onTrimMemory(int level)进行回调,在回调方法中实现资源释放。

    4.使用场景及优化策略

    通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的。一般情况下,有下面几种资源需要进行释放:

    • 缓存:包括一些文件缓存,图片缓存等,在用户正常使用的时候这些缓存很有作用,但当你的应用程序UI不可见的时候,这些缓存就可以被清除以减少内存的使用。比如第三方图片库的缓存。
    • UI:包括一些动态生成动态添加的View等,这些动态生成和添加的View且少数情况下才使用到的View,这时候可以被释放,下次使用的时候再进行动态生成即可。
    更多相关内容
  • 探讨Android内存管理

    2021-01-03 14:51:19
    Android 内存管理机制 如何评估 App 的内存占用 一些减少 App 内存占用的建议 内存管理概述 Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存。这意味着应用修改的任何内存,无论修改的方式是...
  • Android内存管理机制官方详解文档

    千次阅读 2020-12-09 12:40:54
    很早之前写过一篇《Android内存管理机制详解》点击量已7万+,现把Google官方文档整理输出一下,供各位参考。 一、内存管理概览 Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存。这意味着应用...

    很早之前写过一篇《Android内存管理机制详解》点击量已7万+,现把Google官方文档整理输出一下,供各位参考。

    一、内存管理概览

    Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存。这意味着应用修改的任何内存,无论修改的方式是分配新对象还是轻触内存映射的页面,都会一直驻留在 RAM 中,并且无法换出。要从应用中释放内存,只能释放应用保留的对象引用,使内存可供垃圾回收器回收。这种情况有一个例外:对于任何未经修改的内存映射文件(如代码),如果系统想要在其他位置使用其内存,可将其从 RAM 中换出。

    本页面介绍了 Android 如何管理应用进程和内存分配。如需详细了解如何在应用中更高效地管理内存,请参阅管理应用内存。

    垃圾回收

    ART 或 Dalvik 虚拟机之类的受管内存环境会跟踪每次内存分配。一旦确定程序不再使用某块内存,它就会将该内存重新释放到堆中,无需程序员进行任何干预。这种回收受管内存环境中的未使用内存的机制称为“垃圾回收”。垃圾回收有两个目标:在程序中查找将来无法访问的数据对象,并回收这些对象使用的资源。

    Android 的内存堆是分代的,这意味着它会根据分配对象的预期寿命和大小跟踪不同的分配存储分区。例如,最近分配的对象属于“新生代”。当某个对象保持活动状态达足够长的时间时,可将其提升为较老代,然后是永久代。

    堆的每一代对相应对象可占用的内存量都有其自身的专用上限。每当一代开始填满时,系统便会执行垃圾回收事件以释放内存。垃圾回收的持续时间取决于它回收的是哪一代对象以及每一代有多少个活动对象。

    尽管垃圾回收速度非常快,但仍会影响应用的性能。通常情况下,您无法从代码中控制何时发生垃圾回收事件。系统有一套专门确定何时执行垃圾回收的标准。当条件满足时,系统会停止执行进程并开始垃圾回收。如果在动画或音乐播放等密集型处理循环过程中发生垃圾回收,则可能会增加处理时间,进而可能会导致应用中的代码执行超出建议的 16ms 阈值,无法实现高效、流畅的帧渲染。

    此外,您的代码流执行的各种工作可能迫使垃圾回收事件发生得更频繁或导致其持续时间超过正常范围。 例如,如果您在 Alpha 混合动画的每一帧期间,在 for 循环的最内层分配多个对象,则可能会使内存堆受到大量对象的影响。在这种情况下,垃圾回收器会执行多个垃圾回收事件,并可能降低应用的性能。

    如需详细了解有关垃圾回收的一般信息,请参阅垃圾回收。

    共享内存

    为了在 RAM 中容纳所需的一切,Android 会尝试跨进程共享 RAM 页面。它可以通过以下方式实现这一点:

    • 每个应用进程都从一个名为 Zygote 的现有进程分叉。系统启动并加载通用框架代码和资源(如 Activity 主题背景)时,Zygote 进程随之启动。为启动新的应用进程,系统会分叉 Zygote 进程,然后在新进程中加载并运行应用代码。这种方法使为框架代码和资源分配的大多数 RAM 页面可在所有应用进程之间共享。
    • 大多数静态数据会内存映射到一个进程中。这种方法使得数据不仅可以在进程之间共享,还可以在需要时换出。静态数据示例包括:Dalvik 代码(通过将其放入预先链接的 .odex 文件中进行直接内存映射)、应用资源(通过将资源表格设计为可内存映射的结构以及通过对齐 APK 的 zip 条目)和传统项目元素(如 .so 文件中的原生代码)。
    • 在很多地方,Android 使用明确分配的共享内存区域(通过 ashmem 或 gralloc)在进程间共享同一动态 RAM。例如,窗口 surface 使用在应用和屏幕合成器之间共享的内存,而光标缓冲区则使用在内容提供器和客户端之间共享的内存。
      由于共享内存的广泛使用,在确定应用使用的内存量时需要小心谨慎。有关正确确定应用内存使用量的技巧,请参阅调查 RAM 使用量。

    分配与回收应用内存

    Dalvik 堆局限于每个应用进程的单个虚拟内存范围。这定义了逻辑堆大小,该大小可以根据需要增长,但不能超过系统为每个应用定义的上限。

    堆的逻辑大小与堆使用的物理内存量不同。在检查应用堆时,Android 会计算按比例分摊的内存大小 (PSS) 值,该值同时考虑与其他进程共享的脏页和干净页,但其数量与共享该 RAM 的应用数量成正比。此 (PSS) 总量是系统认为的物理内存占用量。有关 PSS 的详情,请参阅调查 RAM 使用量指南。

    Dalvik 堆不压缩堆的逻辑大小,这意味着 Android 不会对堆进行碎片整理来缩减空间。只有当堆末尾存在未使用的空间时,Android 才能缩减逻辑堆大小。但是,系统仍然可以减少堆使用的物理内存。垃圾回收之后,Dalvik 遍历堆并查找未使用的页面,然后使用 madvise 将这些页面返回给内核。因此,大数据块的配对分配和解除分配应该使所有(或几乎所有)使用的物理内存被回收。但是,从较小分配量中回收内存的效率要低得多,因为用于较小分配量的页面可能仍在与其他尚未释放的数据块共享。

    限制应用内存

    为了维持多任务环境的正常运行,Android 会为每个应用的堆大小设置硬性上限。不同设备的确切堆大小上限取决于设备的总体可用 RAM 大小。如果您的应用在达到堆容量上限后尝试分配更多内存,则可能会收到 OutOfMemoryError。

    在某些情况下,例如,为了确定在缓存中保存多少数据比较安全,您可能需要查询系统以确定当前设备上确切可用的堆空间大小。您可以通过调用 getMemoryClass() 向系统查询此数值。此方法返回一个整数,表示应用堆的可用兆字节数。

    切换应用

    当用户在应用之间切换时,Android 会将非前台应用保留在缓存中。非前台应用就是指用户看不到或未运行前台服务(如音乐播放)的应用。例如,当用户首次启动某个应用时,系统会为其创建一个进程;但是当用户离开此应用时,该进程不会退出。系统会将该进程保留在缓存中。如果用户稍后返回该应用,系统就会重复使用该进程,从而加快应用切换速度。

    如果您的应用具有缓存的进程且保留了目前不需要的资源,那么即使用户未使用您的应用,它也会影响系统的整体性能。当系统资源(如内存)不足时,它将会终止缓存中的进程。系统还会考虑终止占用最多内存的进程以释放 RAM。

    注意:当应用处于缓存中时,所占用的内存越少,就越有可能免于被终止并得以快速恢复。但是,系统也可能根据当下的需求不考虑缓存进程的资源使用情况而随时将其终止。

    二、进程间的内存分配

    Android 平台在运行时不会浪费可用的内存。它会一直尝试利用所有可用内存。例如,系统会在应用关闭后将其保留在内存中,以便用户快速切回到这些应用。因此,通常情况下,Android 设备在运行时几乎没有可用的内存。要在重要系统进程和许多用户应用之间正确分配内存,内存管理至关重要。

    本章讨论了 Android 如何为系统和用户应用分配内存的基础知识,另外还说明了操作系统如何应对低内存情况。

    内存类型

    Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。请注意,CPU 和 GPU 访问同一个 RAM。
    在这里插入图片描述
    图 1. 内存类型 - RAM、zRAM 和存储器

    • RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量。
    • zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压缩。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限。
    • 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的对象代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命。

    内存页面

    RAM 分为多个“页面”。通常,每个页面为 4KB 的内存。

    系统会将页面视为“可用”或“已使用”。可用页面是未使用的 RAM。已使用的页面是系统目前正在使用的 RAM,并分为以下类别:

    • 缓存页:有存储器中的文件(例如代码或内存映射文件)支持的内存。缓存内存有两种类型:
      • 私有页:由一个进程拥有且未共享
        • 干净页:存储器中未经修改的文件副本,可由 kswapd 删除以增加可用内存
        • 脏页:存储器中经过修改的文件副本;可由 kswapd 移动到 zRAM 或在 zRAM 中进行压缩以增加可用内存
      • 共享页:由多个进程使用
        • 干净页:存储器中未经修改的文件副本,可由 kswapd 删除以增加可用内存
        • 脏页:存储器中经过修改的文件副本;允许通过 kswapd 或者通过明确使用 msync() 或 munmap() 将更改写回存储器中的文件,以增加可用空间
    • 匿名页:没有存储器中的文件支持的内存(例如,由设置了 MAP_ANONYMOUS 标记的 mmap() 进行分配)
      • 脏页:可由 kswapd 移动到 zRAM/在 zRAM 中进行压缩以增加可用内存

    注意: 干净页包含存在于存储器中的文件(或文件一部分)的精确副本。如果干净页不再包含文件的精确副本(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。
    随着系统积极管理 RAM,可用和已使用页面的比例会不断变化。本部分介绍的概念对于管理内存不足的情况至关重要。本文档的下一部分将对这些概念进行更详细的说明。

    内存不足管理

    Android 有两种处理内存不足情况的主要机制:内核交换守护进程和低内存终止守护进程。
    内核交换守护进程
    内核交换守护进程 (kswapd) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核设有可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd 开始回收内存。当可用内存达到上限阈值时,kswapd 停止回收内存。

    kswapd 可以删除干净页来回收它们,因为这些页受到存储器的支持且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页面从存储器复制到 RAM。此操作称为“请求分页”。
    在这里插入图片描述
    图 2. 由存储器支持的干净页已删除

    kswapd 可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页面关联的进程被终止,则该页面将从 zRAM 中删除。

    如果可用内存量低于特定阈值,系统会开始终止进程。
    在这里插入图片描述
    图 3. 脏页被移至 zRAM 并进行压缩

    低内存终止守护进程
    很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。

    LMK 使用一个名为 oom_adj_score 的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。下表列出了从高到低的 LMK 评分类别。评分最高的类别,即第一行中的项目将最先被终止:
    在这里插入图片描述
    图 4. Android 进程,高分在上,低分在下

    以下是上表中各种类别的说明:

    • 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用。
    • 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
    • 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。
    • 服务:服务由应用启动,可能包括同步或上传到云端。
    • 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
    • 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
    • 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
    • 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
    • 原生:系统使用的极低级别的进程(例如,kswapd)。

    设备制造商可以更改 LMK 的行为。

    计算内存占用量

    内核会跟踪系统中的所有内存页面。
    在这里插入图片描述
    图 5. 不同进程使用的页面

    在确定应用使用的内存量时,系统必须考虑共享的页面。访问相同服务或库的应用将共享内存页面。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。
    在这里插入图片描述
    图 6. 由两个应用共享的页面(中间)

    如需确定应用的内存占用量,可以使用以下任一指标:

    • 常驻内存大小 (RSS):应用使用的共享和非共享页面的数量
    • 按比例分摊的内存大小 (PSS):应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)
    • 独占内存大小 (USS):应用使用的非共享页面数量(不包括共享页面)

    如果操作系统想要知道所有进程使用了多少内存,那么 PSS 非常有用,因为页面只会统计一次。计算 PSS 需要花很长时间,因为系统需要确定共享的页面以及共享页面的进程数量。RSS 不区分共享和非共享页面(因此计算起来更快),更适合跟踪内存分配量的变化。

    三、管理应用内存

    随机存取存储器 (RAM) 在任何软件开发环境中都是一项宝贵资源,但在移动操作系统中,由于物理内存通常都有限,因此 RAM 就更宝贵了。虽然 Android 运行时 (ART) 和 Dalvik 虚拟机都执行例行的垃圾回收任务,但这并不意味着您可以忽略应用分配和释放内存的位置和时间。您仍然需要避免引入内存泄漏问题(通常因在静态成员变量中保留对象引用而引起),并在适当时间(如生命周期回调所定义)释放所有 Reference 对象。

    本页面介绍了如何积极减少应用的内存使用量。如需了解 Android 操作系统如何管理内存,请参阅 Android 内存管理概览。

    监控可用内存和内存使用量

    您需要先找到应用中的内存使用问题,然后才能修复问题。Android Studio 中的内存性能剖析器可以通过以下方式帮助您查找和诊断内存问题:

    了解您的应用在一段时间内如何分配内存。内存分析器可以显示实时图表,说明应用的内存使用量、分配的 Java 对象数量以及垃圾回收事件发生的时间。
    发起垃圾回收事件,并在应用运行时拍摄 Java 堆的快照。
    记录应用的内存分配情况,然后检查所有分配的对象、查看每个分配的堆栈轨迹,并在 Android Studio 编辑器中跳转到相应代码。

    释放内存以响应事件
    如 Android 内存管理概览中所述,Android 可以通过多种方式从应用中回收内存,或在必要时完全终止应用,从而释放内存以执行关键任务。为了进一步帮助平衡系统内存并避免系统需要终止您的应用进程,您可以在 Activity 类中实现 ComponentCallbacks2 接口。借助所提供的 onTrimMemory() 回调方法,您的应用可以在处于前台或后台时监听与内存相关的事件,然后释放对象以响应指示系统需要回收内存的应用生命周期事件或系统事件。

    例如,您可以实现 onTrimMemory() 回调以响应不同的与内存相关的事件,如下所示:

        import android.content.ComponentCallbacks2;
        // Other import statements ...
    
        public class MainActivity extends AppCompatActivity
            implements ComponentCallbacks2 {
    
            // Other activity code ...
    
            /**
             * Release memory when the UI becomes hidden or when system resources become low.
             * @param level the memory-related event that was raised.
             */
            public void onTrimMemory(int level) {
    
                // Determine which lifecycle or system event was raised.
                switch (level) {
    
                    case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
    
                        /*
                           Release any UI objects that currently hold memory.
    
                           The user interface has moved to the background.
                        */
    
                        break;
    
                    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
    
                        /*
                           Release any memory that your app doesn't need to run.
    
                           The device is running low on memory while the app is running.
                           The event raised indicates the severity of the memory-related event.
                           If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                           begin killing background processes.
                        */
    
                        break;
    
                    case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                    case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                    case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
    
                        /*
                           Release as much memory as the process can.
    
                           The app is on the LRU list and the system is running low on memory.
                           The event raised indicates where the app sits within the LRU list.
                           If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                           the first to be terminated.
                        */
    
                        break;
    
                    default:
                        /*
                          Release any non-critical data structures.
    
                          The app received an unrecognized memory level value
                          from the system. Treat this as a generic low-memory message.
                        */
                        break;
                }
            }
        }
    

    Android 4.0(API 级别 14)中添加了 onTrimMemory() 回调。对于早期版本,您可以使用 onLowMemory(),此回调大致相当于 TRIM_MEMORY_COMPLETE 事件。

    查看您应该使用多少内存
    为了允许多个进程同时运行,Android 针对为每个应用分配的堆大小设置了硬性限制。设备的确切堆大小限制因设备总体可用的 RAM 多少而异。如果您的应用已达到堆容量上限并尝试分配更多内存,系统就会抛出 OutOfMemoryError。

    为了避免用尽内存,您可以查询系统以确定当前设备上可用的堆空间。您可以通过调用 getMemoryInfo() 向系统查询此数值。它将返回一个 ActivityManager.MemoryInfo 对象,其中会提供与设备当前的内存状态有关的信息,包括可用内存、总内存和内存阈值(如果达到此内存级别,系统就会开始终止进程)。ActivityManager.MemoryInfo 对象还会提供一个简单的布尔值lowMemory,您可以根据此值确定设备是否内存不足。

    以下代码段示例演示了如何在应用中使用 getMemoryInfo() 方法。

        public void doSomethingMemoryIntensive() {
    
            // Before doing something that requires a lot of memory,
            // check to see whether the device is in a low memory state.
            ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
    
            if (!memoryInfo.lowMemory) {
                // Do memory intensive work ...
            }
        }
    
        // Get a MemoryInfo object for the device's current memory status.
        private ActivityManager.MemoryInfo getAvailableMemory() {
            ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
            ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
            activityManager.getMemoryInfo(memoryInfo);
            return memoryInfo;
        }
    

    使用内存效率更高的代码结构

    某些 Android 功能、Java 类和代码结构所使用的内存往往多于其他功能、类和结构。您可以在代码中选择效率更高的替代方案,以尽可能降低应用的内存使用量。

    谨慎使用服务
    在不需要某项服务时让其保持运行状态,是 Android 应用可能犯下的最严重的内存管理错误之一。如果您的应用需要某项服务在后台执行工作,请不要让其保持运行状态,除非其需要运行作业。请注意在服务完成任务后使其停止运行。否则,您可能会在无意中导致内存泄漏。

    在您启动某项服务后,系统更倾向于让此服务的进程始终保持运行状态。这种行为会导致服务进程代价十分高昂,因为一旦服务使用了某部分 RAM,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现抖动。

    您通常应该避免使用持久性服务,因为它们会对可用内存提出持续性的要求。我们建议您采用 JobSchedulerJobScheduler 等替代实现方式。要详细了解如何使用 JobScheduler 调度后台进程,请参阅后台优化。

    如果您必须使用某项服务,则限制此服务的生命周期的最佳方式是使用 IntentService,它会在处理完启动它的 intent 后立即自行结束。有关详情,请参阅在后台服务中运行。

    使用经过优化的数据容器
    编程语言所提供的部分类并未针对移动设备做出优化。例如,常规 HashMap 实现的内存效率可能十分低下,因为每个映射都需要分别对应一个单独的条目对象。

    Android 框架包含几个经过优化的数据容器,包括 SparseArray、SparseBooleanArray 和 LongSparseArray。 例如,SparseArray 类的效率更高,因为它们可以避免系统需要对键(有时还对值)进行自动装箱(这会为每个条目分别再创建 1-2 个对象)。

    如果需要,您可以随时切换到原始数组以获得非常精简的数据结构。

    谨慎对待代码抽象
    开发者往往会将抽象简单地当做一种良好的编程做法,因为抽象可以提高代码灵活性和维护性。不过,抽象的代价很高:通常它们需要更多的代码才能执行,需要更多的时间和更多的 RAM 才能将代码映射到内存中。因此,如果抽象没有带来显著的好处,您就应该避免使用抽象。

    针对序列化数据使用精简版 Protobuf
    协议缓冲区是 Google 设计的一种无关乎语言和平台,并且可扩展的机制,用于对结构化数据进行序列化。该机制与 XML 类似,但更小、更快也更简单。如果您决定针对数据使用 Protobuf,则应始终在客户端代码中使用精简版 Protobuf。常规 Protobuf 会生成极其冗长的代码,这会导致应用出现多种问题,例如 RAM 使用量增多、APK 大小显著增加以及执行速度变慢。

    有关详情,请参阅 Protobuf 自述文件中的“精简版”部分。

    避免内存抖动
    如前所述,垃圾回收事件通常不会影响应用的性能。不过,如果在短时间内发生许多垃圾回收事件,就可能会快速耗尽帧时间。系统花在垃圾回收上的时间越多,能够花在呈现或流式传输音频等其他任务上的时间就越少。

    通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。

    例如,您可以在 for 循环中分配多个临时对象。或者,您也可以在视图的 onDraw() 函数中创建新的 Paint 或 Bitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗新生代 (young generation) 区域中的所有可用内存,从而迫使垃圾回收事件发生。

    当然,您必须先在代码中找到内存抖动较高的位置,然后才能进行修复。为此,您应该使用 Android Studio 中的内存分析器。

    确定代码中的问题区域后,请尝试减少对性能至关重要的区域中的分配数量。您可以考虑将某些代码逻辑从内部循环中移出,或将其移到基于 Factory 的分配结构中。

    移除会占用大量内存的资源和库

    代码中的某些资源和库可能会在您不知情的情况下吞噬内存。APK 的总体大小(包括第三方库或嵌入式资源)可能会影响应用的内存消耗量。您可以通过从代码中移除任何冗余、不必要或臃肿的组件、资源或库,降低应用的内存消耗量。

    缩减总体 APK 大小
    您可以通过缩减应用的总体大小来显著降低应用的内存使用量。位图大小、资源、动画帧数和第三方库都会影响 APK 的大小。Android Studio 和 Android SDK 提供了可帮助您缩减资源和外部依赖项大小的多种工具。这些工具支持现代代码收缩方法,例如 R8 编译。(Android Studio 3.3 及更低版本使用 ProGuard,而不是 R8 编译。)

    要详细了解如何缩减 APK 的总体大小,请参阅有关如何缩减应用大小的指南。

    使用 Dagger 2 实现依赖注入
    依赖注入框架可以简化您编写的代码,并提供一个可供您进行测试及其他配置更改的自适应环境。

    如果您打算在应用中使用依赖注入框架,请考虑使用 Dagger 2。Dagger 不使用反射来扫描您应用的代码。Dagger 的静态编译时实现意味着它可以在 Android 应用中使用,而不会带来不必要的运行时代价或内存消耗量。

    其他使用反射的依赖注入框架倾向于通过扫描代码中的注释来初始化进程。此过程可能需要更多的 CPU 周期和 RAM,并可能在应用启动时导致出现明显的延迟。

    谨慎使用外部库
    外部库代码通常不是针对移动环境编写的,在移动客户端上运行时可能效率低下。如果您决定使用外部库,则可能需要针对移动设备优化该库。在决定是否使用该库之前,请提前规划,并在代码大小和 RAM 消耗量方面对库进行分析。

    即使是一些针对移动设备进行了优化的库,也可能因实现方式不同而导致问题。例如,一个库可能使用的是精简版 Protobuf,而另一个库使用的是 Micro Protobuf,导致您的应用出现两种不同的 Protobuf 实现。日志记录、分析、图像加载框架和缓存以及许多您意料之外的其他功能的不同实现都可能导致这种情况。

    虽然 ProGuard 可以使用适当的标记移除 API 和资源,但无法移除库的大型内部依赖项。您所需要的这些库中的功能可能需要较低级别的依赖项。如果存在以下情况,这就特别容易导致出现问题:您使用某个库中的 Activity 子类(往往会有大量的依赖项)、库使用反射(这很常见,意味着您需要花费大量的时间手动调整 ProGuard 以使其运行)等。

    此外,请避免仅针对数十个功能中的一两个功能使用共享库。您一定不希望产生大量您甚至根本用不到的代码和开销。在考虑是否使用某个库时,请查找与您的需求十分契合的实现。否则,您可以决定自己去创建实现。

    本文翻译整理自Google说明文档《管理应用内存》

    展开全文
  • Android 内存管理详解

    千次阅读 2021-02-20 18:29:04
    和你一起终身学习,这里是程序员Android经典好文推荐,通过阅读本文,您将收获以下知识点:一、Android 垃圾回收机制(GC)二、共享内存三、内存的申请与回收四、内存限制五、不同Ap...

    和你一起终身学习,这里是程序员Android

    经典好文推荐,通过阅读本文,您将收获以下知识点:

    一、Android 垃圾回收机制(GC)
    二、共享内存
    三、内存的申请与回收
    四、内存限制
    五、不同App切换时的内存管理

    Android Runtime(ART)Dalvik虚拟机使用 分页 和 内存映射 来管理内存。这意味着应用程序修改的任何内存(无论是通过分配新对象通过映射页面)都将保留在RAM中,并且不能被分页。
    应用程序释放内存的唯一方法是释放应用程序持有的对象引用,即使垃圾收集器回收(GC)回收内存 。
    比如:如果系统想要在其他地方使用该内存,则可以将任何未经修改的映射到mmap中文件(例如代码)分页出RAM

    本页面介绍了Android如何管理应用程序进程和内存分配。有关如何在应用程序中更高效地管理内存的更多信息,请参阅管理您的应用程序的内存。

    一、Android 垃圾回收机制(GC)

    ART 或 Dalvik虚拟机的托管内存环境会跟踪每个内存分配情况。一旦它确定一段内存不再被程序使用,它将它释放回堆(Heap)中,并且不需要程序员的任何干预。回收托管内存环境中未使用内存的机制称为垃圾回收(GC)

    1.垃圾回收机制的目的

    垃圾回收机制的目的:
    1.在程序中查找将来不再使用的数据对象(Object)
    2.回收这些对象所占用的内存资源。

    Android 内存是一个典型的堆内存(Heap),这意味着系统会根据被分配的对象的预期寿命和大小有不同的分配桶。
    例如,最近分配的对象属于年轻一代。当一个对象保持足够的活动时间时,它可以被提升到一个老一代,然后是一个永久的时代。

    每个堆的生成都有自己占用内存的上限。任何时候一旦内存即将被填满,系统就会执行一个垃圾回收事件去试图释放内存。垃圾收集的持续时间取决于它正在回收的那个对象以及回收多少个正在活跃的对象。

    尽管垃圾器回收的速度相当快,但它仍然可能会影响应用程序的性能。并且你一般不会控制垃圾器回收你代码事件的时间。
    系统垃圾回收机制具有一定运行中的标准,这个标准主要用于确定何时执行垃圾收集。当条件满足时,系统停止执行进程并开始垃圾收集。

    如果垃圾回收发生在密集的处理循环(如动画)或音乐播放期间,可能会增加处理时间。这种增长可能会推动您的应用程序执行代码超过推荐的16ms阈值,以实现高效流畅的帧渲染。

    此外,您的代码流可能会执行各种强制垃圾收集事件的工作,或使其持续时间超过正常。例如,如果在alpha混合动画的每个帧期间在for循环的最内部分配了多个对象,则可能会占用大量的内存堆,并使用大量对象。在这种情况下,垃圾收集器将执行多个垃圾收集事件,并可能降低应用程序的性能。

    有关垃圾收集的更多一般信息,请参阅垃圾收集。

    二、共享内存

    为了适应不同的RAM需求,Android 尝试在不同进程之间共享内存,共享内存的方法如下:

    1. APP共享Framework框架代码以及资源

    每个APP的进程都是从Zygote进程中分离出来的。
    Zygote 进程:
    Zygote进程是在系统启动并加载Framwork框架代码和资源(如Activity Theme)时开始。要启动一个新的应用程序进程,系统会从Zygote进程fork分离出来,然后在新进程中加载并运行应用程序的代码。这种方法允许大部分分配给Framework框架代码和资源的RAM页面与所有应用程序进程之间共享。

    2. 大多数静态数据可以跨进程共享

    大多数静态数据被映射到一个进程。这种技术允许数据在进程之间共享,并允许在需要时将其分页。示例静态数据包括:Dalvik代码(通过将其放置在用于直接映射的预链接.odex文件中),应用程序资源(通过将资源表设计为可以被映射的结构以及通过对齐APK的zip条目) 以及.so文件中的本地代码等传统项目元素。

    3. Android使用显式分配的共享内存区域(使用ashmem或gralloc)共享同一个动态RAM。

    例如,窗口表面使用应用程序和屏幕合成器之间的共享内存,游标缓冲区使用内容提供者和客户端之间的共享内存。

    由于共享内存的广泛使用,确定您的应用使用多少内存需要谨慎。在调查您的RAM使用情况中讨论了正确确定应用程序内存使用情况的技巧。如需获取更多内容,请看RAM使用情况分析详解。

    三、APP内存的申请与回收

    Dalvik 虚拟机会限制每个应用程序进程的虚拟内存范围。它定义了逻辑堆的大小,并且可以根据需要增长,但只能达到系统为每个应用程序定义的限制。

    堆的逻辑大小与堆使用的物理内存量不同。在检查应用程序的堆时,Android 会计算一个名为“比例集合大小** Proportion Set Size ”(PSS)** 的值,该值与其他进程共享的脏页面(Page)和干净页面(Page),并且数量与多少应用程序共享该RAM成正比。(PSS)总数是系统认为是您的物理内存的足迹。有关PSS的更多信息,请参阅RAM使用情况指南

    Dalvik堆不压缩堆的逻辑大小,这意味着 Android不会整理堆以关闭空间。当堆的末尾有未使用的空间时,Android只能缩小逻辑堆的大小。然而,系统仍然可以减少堆使用的物理内存。在垃圾收集之后,Dalvik遍历堆并找到未使用的页面,然后使用madvise将这些页面返回给内核。
    因此,成对的大块分配和释放应该会导致所有(或几乎全部)所使用的物理内存的回收。但是,从小分配中回收内存的效率可能会低得多,因为用于小分配的页面仍可能与尚未释放的其他内容共享。

    四、APP内存限制

    为了维护一个主要的多功能环境,Android 对每个应用程序的堆大小设定了一个硬限制。确切的堆大小限制根据设备有多少RAM可用而有所不同。如果您的应用程序已达到堆容量并尝试分配更多内存,则可能会收到OutOfMemoryError

    在某些情况下,您可能需要查询系统以确定在当前设备上有多少堆空间可用 。
    例如,确定有多少数据可以安全地保留在缓存中。你可以通过调用getMemoryClass()来查询这个数字。此方法返回一个整数,指示可用于应用程序堆的兆字节数。

    五、不同App切换时的内存管理

    当用户在应用程序之间进行切换时,Android会保留不在前台的应用程序(即对用户不可见,或者在最近最少使用(LRU)缓存中运行诸如音乐播放之类的前台服务)。例如,当用户第一次启动应用程序时,会为其创建一个进程; 但是当用户离开应用程序时,该过程不会退出。系统保持进程缓存。如果用户稍后返回到应用程序,系统将重新使用该进程,从而使应用程序切换更快。

    如果您的应用程序具有缓存的进程,并且保留了当前不需要的内存,那么您的应用程序(即使用户不使用它)也会影响系统的整体性能。由于系统内存不足,它会以最近使用最少的进程开始,终止LRU缓存中的进程。系统也考虑到保存最多内存的进程,并可以终止它们以释放RAM

    注意:当系统开始在LRU缓存中查杀进程时,它主要是自下而上的。系统也考虑哪些进程消耗更多的内存,从而为系统提供更多的内存增益。在整个LRU列表中消耗的内存越少,留在列表中的机会就越好,并能够快速恢复。

    友情推荐:

    Android 开发干货集锦

    至此,本篇已结束。转载网络的文章,小编觉得很优秀,欢迎点击阅读原文,支持原创作者,如有侵权,恳请联系小编删除,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

    点个在看,方便您使用时快速查找!

    展开全文
  • Android 内存管理

    千次阅读 2020-01-16 16:42:56
    Android 的性能优化的各个部分里,内存的问题绝对是最令人头疼的一部分,虽然 Android 有垃圾自动回收机制不需要手动干预,但也恰因为此,出现内存问题如内存泄漏和内存溢出等,如果对内存管理机制不熟悉,会更加...

    初识内存优化

    在 Android 的性能优化的各个部分里,内存的问题绝对是最令人头疼的一部分,虽然 Android 有垃圾自动回收机制不需要手动干预,但也恰因为此,出现内存问题如内存泄漏和内存溢出等,如果对内存管理机制不熟悉,会更加难以排查问题。

    内存分配

    谈 Android 的内存,就不能不提 Java 的内存管理。Java 程序在运行的过程中会将其管理的内存分为若干个不同的数据区
    在这里插入图片描述
    方法区:方法区存放的是类信息、常量、静态变量,所有线程共享区域。

    虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,线程私有区域。

    本地方法栈:与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的 Native 方法服务。

    :JVM 管理的内存中最大的一块,所有线程共享;用来存放对象实例,几乎所有的对象实例都在堆上分配内存;此区域也是垃圾回收器(Garbage Collection)主要的作用区域,内存泄漏就发生在这个区域。

    程序计数器:可看做是当前线程所执行的字节码的行号指示器;如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是 Native 方法,这个计数器的值为空(Undefined)。

    备注:有一种习惯说法:把Java的内存区域分为堆内存(Heap)和栈内存(Stack),Stack 访问快,Heap 访问慢,Stack 中保存的是对象的引用(指针),Heap 中保存的是对象的实例。实际上这种说法是笼统、粗糙的,此处所说的 Stack 仅仅是虚拟机栈中的局部变量表部分。虚拟机栈与 JVM 运行时数据区涵盖的都比此种说法多。

    内存回收

    1、标记-清除算法

    最基础的收集算法:分为“标记”和“清除”两个阶段,首先,标记出所有需要回收的对象,然后统一回收所有被标记的对象。
    这种方法有两个不足点:

    效率问题,标记和清除两个过程的效率都不高;
    空间问题,标记清除之后会产生大量的不连续的内存碎片。

    在这里插入图片描述

    2、复制算法

    将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存将用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。
    这种方法的特点:

    优点:实现简单,运行高效;每次都是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可;
    缺点:粗暴的将内存缩小为原来的一半,代价实在有点高。
    在这里插入图片描述

    3、标记-整理算法

    先标记需要回收的对象(标记过程与“标记-清除”算法一样),然后把所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    这种方法的特点:

    避免了内存碎片;
    避免了“复制”算法50%的空间浪费;
    主要针对对象存活率高的老年代。
    在这里插入图片描述

    4、分代收集算法

    根据对象的存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

    对象是否回收的依据

    1、引用计数算法

    给对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器值加 1;引用失效时,计数器值减 1;任意时刻计数器为 0 的对象就是不可能再被使用的,表示该对象不存在引用关系。
    这种方法的特点:

    优点:实现简单,判定效率也很高;
    缺点:难以解决对象之间相互循环引用导致计数器值不等于 0 的问题。

    2、可达性分析算法

    以一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(GC Roots 到这个对象不可达),则证明此对象是不可用的。
    在这里插入图片描述

    Android的内存管理

    Android 系统的 ART 和 Dalvik 虚拟机扮演了常规的内存垃圾自动回收的角色, 使用 paging 和 memory-mapping 来管理内存,这意味着不管是因为创建对象还是使用使用内存页面造成的任何被修改的内存,都会一直存在于内存中,App 唯一释放内存的方法就是释放 App 持有的对象引用,使 GC 可以回收。
    在这里插入图片描述

    1、内存回收

    在 Android 的高级系统版本里面针对 Heap 空间有一个 Generational Heap Memory 的模型,最近分配的对象会存放在 Young Generation 区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到 Old Generation,最后累积一定时间再移动到 Permanent Generation 区域。系统会根据内存中不同的内存数据类型分别执行不同的 gc 操作。例如,刚分配到 Young Generation 区域的对象通常更容易被销毁回收,同时在Young Generation 区域的 gc 操作速度会比 Old Generation 区域的 gc 操作速度更快。

    2、共享内存

    Android应用的进程都是从一个叫做 Zygote 的进程 fork 出来的。Zygote 进程在系统启动并且载入通用的framework 的代码与资源之后开始启动。为了启动一个新的程序进程,系统会 fork Zygote 进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这使得大多数的 RAM pages 被用来分配给 framework的代码,同时使得 RAM 资源能够在应用的所有进程之间进行共享。
    大多数 static 的数据被 mmapped 到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被 paged out。常见的 static 数据包括 Dalvik Code,app resources,so 文件等。
    大多数情况下,Android 通过显式的分配共享内存区域(例如 ashmem 或者 gralloc)来实现动态 RAM 区域能够在不同进程之间进行共享的机制。例如,Window Surface 在 App 与 Screen Compositor 之间使用共享的内存,Cursor Buffers 在 Content Provider 与 Clients 之间共享内存。

    3、分配与回收内存

    每一个进程的 Dalvik heap 都反映了使用内存的占用范围。这就是通常逻辑意义上提到的 Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。
    逻辑上讲的 Heap Size 和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。

    4、限制应用的内存

    为了整个 Android 系统的内存控制需要,Android 系统为每一个应用程序都设置了一个硬性的 Dalvik Heap Size 最大限制阈值,这个阈值在不同的设备上会因为 RAM 大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起 OutOfMemoryError 的错误。
    ActivityManager.getMemoryClass() 可以用来查询当前应用的 Heap Size 阈值,这个方法会返回一个整数,表明你的应用的 Heap Size 阈值是多少 Mb(megabates)。

    5、应用切换

    Android 系统并不会在用户切换应用的时候做交换内存的操作。Android 会把那些不包含 Foreground 组件的应用进程放到 LRU Cache 中。例如,当用户开始启动了一个应用,系统会为它创建了一个进程,但是当用户离开这个应用,此进程并不会立即被销毁,而是会被放到系统的 Cache 当中,如果用户后来再切换回到这个应用,此进程就能够被马上完整的恢复,从而实现应用的快速切换。
    如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此当系统开始进入 Low Memory 的状态时,它会由系统根据 LRU 的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。

    需要特别注意的:在 Dalvik 下,大部分 Davik 采取的都是标记-清理回收算法,而且具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。标记-清理回收算法无法对 Heap 中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断 Heap 的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间;这样内存空洞就产生了。
    在这里插入图片描述
    如上图所示,第一行,在开始阶段,内存分配较满;第二行,经过GC之后,大部分对象被释放。此时可能产生的问题是,因为没有内存整理功能,整个页面的 4KB 内存(内存分配的最小单位是页面,通常为 4KB)可能只有一个小对象,但是统计 PrivateDirty/Pss 时还是按照 4KB 计算。所以对于 Dalvik 虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView, onDraw 等函数中 new 对象),另一个又要尽量去避免产生很多长生命周期的大对象。
    ART 在 GC 上不像 Dalvik 仅有一种回收算法,ART 在不同的情况下会选择不同的回收算法。应用程序在前台运行时,响应性是最重要的,因此也要求执行的 GC 是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC 适合作为 Foreground GC,而Mark-Compact GC 适合作为 Background GC。由于有 Compact 的能力存在,内存碎片在ART上可以很好的被避免,这个也是 ART 一个很好的能力。

    Android GC何时发生

    由上文我们知道,GC 操作主要是由系统决定的,但是我们可以监听系统的 GC 过程,以此来分析我们应用程序当前的内存状态。
    Dalvik 虚拟机,每一次 GC 打印内容格式:

    D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
    

    含义解析

    GC_Reason:GC 触发原因
    GC_CONCURRENT:当已分配内存达到某一值时,触发并发 GC;
    GC_FOR_MALLOC:当尝试在堆上分配内存不足时触发的 GC;系统必须停止应用程序并回收内存;
    GC_HPROF_DUMP_HEAP: 当需要创建 HPROF 文件来分析堆内存时触发的 GC;
    GC_EXPLICIT:当明确的调用 GC 时,例如调用 System.gc() 或者通过 DDMS 工具显式地告诉系统进行 GC 操作等;
    GC_EXTERNAL_ALLOC: 仅在 API 级别为 10 或者更低时(新版本分配内存都在 Dalvik 堆上)
    Amount_freed_GC:回收的内存大小
    Heap_stats:堆上的空闲内存百分比 (已用内存)/(堆上总内存)
    External_memory_stats: API 级别为 10 或者更低:(已分配的内存量)/ (即将发生垃圾的极限)
    Pause_time:这次 GC 操作导致应用程序暂停的时间。关于这个暂停的时间,在 2.3 之前 GC 操作是不能并发进行的,也就是系统正在进行 GC,那么应用程序就只能阻塞住等待 GC 结束。而自 2.3 之后,GC 操作改成了并发的方式进行,就是说 GC 的过程中不会影响到应用程序的正常运行,但是在 GC 操作的开始和结束的时候会短暂阻塞一段时间。

    Art 虚拟机,每一次 GC 打印内容格式:

    I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>
    

    基本情况和 Dalvik 没有什么差别,GC 的 Reason 更多了,还多了一个 LOS_Space_Status.

    LOS_Space_Status:Large Object Space,大对象占用的空间,这部分内存并不是分配在堆上的,但仍属于应用程序内存空间,主要用来管理 Bitmap 等占内存大的对象,避免因分配大内存导致堆频繁 GC。

    获取内存使用情况

    通过命令行 adb shell dumpsys meminfo packagename 查看内存详细占用情况:
    在这里插入图片描述
    其中几个关键的数据:

    Private(Clean和Dirty的):应用进程单独使用的内存,代表着系统杀死你的进程后可以实际回收的内存总量 **。通常需要特别关注其中更为昂贵的 dirty 部分,它不仅只被你的进程使用而且会持续占用内存而不能被从内存中置换出存储。申请的全部 Dalvik 和本地 heap 内存都是 Dirty 的,和 Zygote 共享的 Dalvik 和本地 heap 内存也都是 Dirty 的。

    Dalvik Heap:Dalvik虚拟机使用的内存,包含 dalvik-heap 和 dalvik-zygote,堆内存,所有的 Java 对象实例都放在这里。

    Heap Alloc:累加了 Dalvik 和 Native 的 heap。

    PSS:这是加入与其他进程共享的分页内存后你的应用占用的内存量,你的进程单独使用的全部内存也会加入这个值里,多进程共享的内存按照共享比例添加到 PSS 值中。如一个内存分页被两个进程共享,每个进程的 PSS 值会包括此内存分页大小的一半在内。
    Dalvik Pss 内存 = 私有内存 Private Dirty + (共享内存 Shared Dirty / 共享进程数)

    TOTAL:上面全部条目的累加值,全局的展示了你的进程占用的内存情况。

    ViewRootImpl:应用进程里的活动窗口视图个数,可以用来监测对话框或者其他窗口的内存泄露。

    AppContexts及Activities:应用进程里 Context 和 Activity 的对象个数,可以用来监测 Activity 的内存泄露。

    原文链接https://www.jianshu.com/p/c4b283848970

    展开全文
  • Android 内存管理机制

    千次阅读 多人点赞 2018-09-03 14:41:45
    本文主要包括三大部分内容: 内存管理基础:从整个计算机领域... Android内存管理相关知识:Android又不同于Linux,它是一个移动操作系统,因此其内存管理上也有自己的特性,这一部分详细讲述Android内存管理...
  • android内存管理

    千次阅读 2016-11-12 15:01:56
    android内存管理与内存泄漏 应用场景 我们在开发应用程序的时候,了解其内存的运作机制以及如何防止内存泄漏都是非常重要的,内存问题也是android面试中经常问到话题。今天我就讲讲有关android内存方面的知识吧。 ...
  • Android内存管理机制

    千次阅读 2018-06-29 12:29:42
    解读 Andriod 内存管理机制 一、进程类型 1、前台进程(foreground):目前正在屏幕上显示的进程和一些系统进程。举例来说, Dialer Storage,Google Search
  • Android内存管理基础

    千次阅读 2021-11-09 20:49:18
    内存管理概览:https://developer.android.com/topic/performance/memory-overview 内存分配:https://developer.android.com/topic/performance/memory-management Android Studio Profiler:...
  • Android内存管理小结

    2015-06-14 22:46:53
    Android内存管理小结 Android内存管理和分析
  • Android内存管理机制详解

    万次阅读 多人点赞 2012-12-13 10:37:33
    这是Linux内存管理的一个优秀特性,在这方面,区别于 Windows的内存管理。主要特点是,无论物理内存有多大,Linux都将其充份利用,将一些程序调用过的硬盘数据读入内存,利用内存读写的高速特性来提高Linux系统的...
  • 本文将集中分析Android内存管理,因为Android系统是在Linux系统的基础上发展起来的,所以在介绍Linux基本的内存管理的基础上对Android内存管理进行研究。
  • 二维码简介及Android 内存管理 MAT内存分析工具
  • android 内存管理

    千次阅读 2012-09-20 16:12:07
    为适应其作为移动平台操作系统的特殊需要,谷歌对其做了特别的设计与优化,使应用程序关闭但不退出,并由操作系统进行进程的回收管理。本文在 Application Framework 与 Linux 内核两个层次上,以进程为粒度,对 ...
  • Android应用内存管理机制

    千次阅读 2021-06-15 19:43:35
    有个客户统计了APPs 统计的数据(我司的产品是Android 机顶盒),跟实际每个APK 统计的数据对不上,需要我们给出解释。客户的问题总结起来有三个: App总内存和所有Apk内存之和不对应:是否有系统APK是隐藏没有出现...
  • android内存管理机制分析,帮助你了解内存管理原理,更好的开发程序
  • 主要介绍android内存管理 以及MAT、DDMS等工具的使用
  • 请教各位大神,我用的一个activity打开一个pdf,完了每次退出的时候我有清空而且提醒内存回收,但是每次内存都不回收,每次打开过一个activity就会消耗掉一些内存,打开几个之后就会内存溢出了,大神们给个建议啊?...
  • 关于android内存溢出的检查,MAT的使用方法和内存溢出的防范方法,看完你会受益匪浅

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 141,012
精华内容 56,404
关键字:

android内存管理