内存管理 订阅
内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。一个执行中的程式,譬如网页浏览器在个人电脑或是图灵机(Turing machine)里面,为一个行程将资料转换于真实世界及电脑内存之间,然后将资料存于电脑内存内部(在计算机科学,一个程式是一群指令的集合,一个行程是电脑在执行中的程式)。一个程式结构由以下两部分而成:“本文区段”,也就是指令存放,提供CPU使用及执行; “资料区段”,储存程式内部本身设定的资料,例如常数字串。 展开全文
内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。一个执行中的程式,譬如网页浏览器在个人电脑或是图灵机(Turing machine)里面,为一个行程将资料转换于真实世界及电脑内存之间,然后将资料存于电脑内存内部(在计算机科学,一个程式是一群指令的集合,一个行程是电脑在执行中的程式)。一个程式结构由以下两部分而成:“本文区段”,也就是指令存放,提供CPU使用及执行; “资料区段”,储存程式内部本身设定的资料,例如常数字串。
信息
概    念
是指软件运行时对计
作用 
高效,快
媒    介
磁带或是磁盘
中文名
内存管理
学    科
计算机
外文名
memory management
内存管理技术简介
内存可以通过许多媒介实现,例如磁带或是磁盘,或是小阵列容量的微芯片。 从1950年代开始,计算机变的更复杂,它内部由许多种类的内存组成。内存管理的任务也变的更加复杂,甚至必须在一台机器同时执行多个进程。虚拟内存是内存管理技术的一个极其实用的创新。它是一段程序(由操作系统调度),持续监控着所有物理内存中的代码段、数据段,并保证他们在运行中的效率以及可靠性,对于每个用户层(user-level)的进程分配一段虚拟内存空间。当进程建立时,不需要在物理内存件之间搬移数据,数据储存于磁盘内的虚拟内存空间,也不需要为该进程去配置主内存空间,只有当该进程被被调用的时候才会被加载到主内存。可以想像一个很大的程序,当他执行时被操作系统调用,其运行需要的内存数据都被存到磁盘内的虚拟内存,只有需要用到的部分才被加载到主内存内部运行。 [1] 
收起全文
精华内容
下载资源
问答
  • 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说明文档《管理应用内存》

    展开全文
  • 分页内存管理

    千次阅读 2019-03-18 16:31:41
    文章目录一、分页内存管理详解1、分页内存管理的核心思想2、分页内存管理能解决什么问题?3、虚拟地址的构成与地址翻译4、页表5、分页内存管理的优缺点二、分页内存管理例子解析三、缺页中断和页面置换的目标1、缺页...

    一、分页内存管理详解

    1、分页内存管理的核心思想

      固定分区会产生内部碎片,动态分区会产生外部碎片,这两种技术对内存的利用率都比较低。我们希望内存的使用能尽量避免碎片的产生,这就引入了分页的思想:将虚拟内存空间和物理内存空间皆划分为大小相同的页面,如4KB、8KB或16KB等,并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。

      分页的最大作用就在于:使得进程的物理地址空间可以是非连续的。

    2、分页内存管理能解决什么问题?

    • 解决空间浪费碎片化问题(外部碎片):由于将虚拟内存空间和物理内存空间按照某种规定的大小进行分配,这里我们称之为页(Page),然后按照页进行内存分配,也就克服了外部碎片的问题。

    • 解决程序大小受限问题:程序增长有限是因为一个程序需要全部加载到内存才能运行,因此解决的办法就是使得一个程序无须全部加载就可以运行。使用分页也可以解决这个问题,只需将当前需要的页面放在内存里,其他暂时不用的页面放在磁盘上,这样一个程序同时占用内存和磁盘,其增长空间就大大增加了。而且,分页之后,如果一个程序需要更多的空间,给其分配一个新页即可(而无需将程序倒出倒进从而提高空间增长效率)。

    3、虚拟地址的构成与地址翻译

    【虚拟地址的构成】:

      在分页系统下,一个程序发出的虚拟地址由两部分组成:页面号和页内偏移值,如下图所示:
    在这里插入图片描述
    例如,对于32位寻址的系统,如果页面大小为4KB,则页面号占20位,页内偏移值占12位。

    【地址翻译:虚拟地址→物理地址】:

      分页系统的核心是页面的翻译,即从虚拟页面到物理页面的映射(Mapping)。而这个翻译过程由内存管理单元(MMU)完成,MMU接收CPU发出的虚拟地址,将其翻译为物理地址后发送给内存。内存管理单元按照该物理地址进行相应访问后读出或写入相关数据,如下图所示:
    在这里插入图片描述
      那么,这个翻译是怎么实现的呢?答案是查页表,对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系。当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。
    在这里插入图片描述

    4、页表

      页表的根本功能是提供从虚拟页面到物理页面的映射。因此,页表的记录条数与虚拟页面数相同。此外,内存管理单元依赖于页表来进行一切与页面有关的管理活动,这些活动包括判断某一页面号是否在内存里,页面是否受到保护,页面是否非法空间等等。
    在这里插入图片描述
      页表的存储方式是TLB(Translation look-aside buffer, 转换表缓冲区)+内存。TLB实际上是一组硬件缓冲所关联的快速内存。若没有TLB,操作系统需要两次内存访问来完成逻辑地址到物理地址的转换,访问页表算一次,在页表中查找算一次。TBL中存储页表中的一小部分条目,条目以键值对方式存储。

    5、分页内存管理的优缺点

    【优点】:

    • 分页系统不会产生外部碎片一个进程占用的内存空间可以不是连续的,并且一个进程的虚拟页面在不需要的时候可以放在磁盘中。

    • 分页系统可以共享小的地址,即页面共享。只需要在对应给定页面的页表项里做一个相关的记录即可。

    【缺点】:

    • 页表很大,占用了大量的内存空间

    • 还是会存在内部碎片。

    二、分页内存管理例子解析

      把物理内存,按照某种尺寸,进行平均分割。比如我现在以2个内存单位,来分割内存,也就是每两个连续的内存空间,组成一个内存页:
    在这里插入图片描述
      接着,系统同样需要维护一个内存信息表:
    在这里插入图片描述
      现在,程序申请长度为3的内存空间,不过由于现在申请的最小单位为页面,而一个页面的长度为2,因此现在需要申请2个页面,也就是4个内存空间。这就浪费了1个内存空间。接着,程序再申请长度为1,长度为2的空间:
    在这里插入图片描述
      释放掉ID=2,内存页ID为3的那条内存空间信息:
    在这里插入图片描述
      现在,就出现了之前的情况:目前一共有4个内存空间,但是不连续。不过,因为现在是分页管理机制,因此,现在仍然可以继续申请长度为4的内存空间:
    在这里插入图片描述

    三、缺页中断和页面置换的目标

    1、缺页中断

      在分页系统中,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。如果CPU发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断,而缺页中断服务程序负责将需要的虚拟页面找到并加载到内存。
    在这里插入图片描述

    2、页面置换的目标

      如果发生了缺页中断,就需要从磁盘上将需要的页面调入内存。如果内存没有多余的空间,就需要在现有的页面中选择一个页面进行替换。使用不同的页面置换算法,页面更换的顺序也会各不相同。如果挑选的页面是之后很快又要被访问的页面,那么系统将很开再次产生缺页中断,因为磁盘访问速度远远内存访问速度,缺页中断的代价是非常大的。因此,挑选哪个页面进行置换不是随随便便的事情,而是有要求的。

      页面置换时挑选页面的目标主要在于降低随后发生缺页中断的次数或概率(也可以说缺页率最低)。因此,挑选的页面应当是随后相当长时间内不会被访问的页面,最好是再也不会被访问的页面

    四、页面置换算法

    1、最佳置换算法(OPT)

      所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。

    2、最近最久未使用(LRU)

      虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近一段时间最久未使用的页面换出。

      为了实现 LRU,需要在内存中维护一个所有页面的链表当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。当发生缺页中断时,删除链表尾部最久没有使用的页面。

    在这里插入图片描述
      因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。

    3、最近未使用(NRU)

      每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:

    • R=0,M=0
    • R=0,M=1
    • R=1,M=0
    • R=1,M=1

      当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

    4、先进先出(FIFO)

      顾名思义,先进先出(FIFO,First In First Out)算法的核心是更换最早进入内存的页面,其实现机制是使用链表将所有在内存中的页面按照进入时间的早晚链接起来,然后每次置换链表头上的页面就行了,而新加进来的页面则挂在链表的末端,如下图所示:
    在这里插入图片描述
      FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:

      当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。
    在这里插入图片描述

    5、时钟

      时钟算法的核心思想是:将页面排成一个时钟的形状,该时钟有一个指针,每次需要更换页面时,我们从指针所指的页面开始检查如果当前页面的访问位为0,即从上次检查到这次,该页面没有被访问过,将该页面替换反之,就将其访问位清零,并顺时针移动指针到下一个页面。重复这些步骤,直到找到一个访问位为0的页面。

      例如下图所示的一个时钟,指针指向的页面是F,因此第一个被考虑替换的页面是F。如果页面F的访问位为0,F将被替换。如果F的访问位为1,则F的访问位清零,指针移动到页面G。
    在这里插入图片描述

    参考:https://blog.csdn.net/tong5956/article/details/74937178
    https://www.cnblogs.com/edisonchou/p/5094066.html

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

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

    本文主要包括三大部分内容:

    内存管理基础:从整个计算机领域简述主要的内存管理技术。
    Linux的内存管理机制:Android毕竟是基于Linux内核实现的操作系统,因此有必要了解一下Linux的内存管理机制。
    Android的内存管理相关知识:Android又不同于Linux,它是一个移动操作系统,因此其内存管理上也有自己的特性,这一部分详细讲述Android的内存管理相关知识,包括 内存管理机制进程管理

    内存管理基础

    概述

    CPU只能访问其寄存器(Register)和内存(Memory), 无法直接访问硬盘(Disk)。 存储在硬盘上的数据必须首先传输到内存中才能被CPU访问。从访问速度来看,对寄存器的访问非常快,通常为1纳秒; 对内存的访问相对较慢,通常为100纳秒(使用缓存加速的情况下);而对硬盘驱动器的访问速度最慢,通常为10毫秒。

    寄存器(Register):CPU内部的高速存储区域

    当一个程序加载到内存中时,它由四个内存区域组成:

    • 堆栈(Stack):存储由该程序的每个函数创建的临时变量
    • 堆(Heap):该区域特别适用于动态内存分配
    • 数据(Data):存储该程序的全局变量和静态变量
    • 代码(Code):存储该程序的指令

    主要的内存管理技术

    • Base and limit registers(基址寄存器和界限寄存器)
    • Virtual memory(虚拟内存)
    • Swapping(交换)
    • Segmentation(分段)
    • Paging(分页)
    Base and limit registers(基址寄存器和界限寄存器)

    必须限制进程,以便它们只能访问属于该特定进程的内存位置。

    每个进程都有一个基址寄存器和限制寄存器:

    • 基址寄存器保存最小的有效存储器地址
    • 限制寄存器指定范围的大小

    例如,process 2的有效内存地址是300040到420940

    那么每个来自用户进程的内存访问都将首先针对这两个寄存器进行一次检查:

    操作系统内核可以访问所有内存位置,因为它需要管理整个内存。

    Virtual memory(虚拟内存)

    虚拟内存(VM)是OS为内存管理提供的基本抽象。

    • 所有程序都使用虚拟内存地址
    • 虚拟地址会被转换为物理地址
    • 物理地址表示数据的实际物理位置
    • 物理位置可以是内存或磁盘

    虚拟地址到物理地址的转换由存储器管理单元(MMU - Memory Management Unit)处理。MMU使用重定位寄存器(relocation register),其值在硬件级别上被添加到每个内存请求中。

    Swapping(交换)

    交换是一种可以暂时将进程从内存交换到后备存储,而之后又可以将其返回内存以继续执行的技术。

    后备存储通常是一个硬盘驱动器,其访问速度快,且大小足以存储内存映像的副本。

    如果没有足够的可用内存来同时保留内存中的所有正在运行的进程,则某些当前未使用CPU的进程可能会被交换到后备存储中。

    交换是一个非常缓慢的过程。 主要耗时部分是数据传输。例如,如果进程占用10MB内存并且后备存储的传输速率为40MB/秒,则需要0.25秒来进行数据传输。 再加上将数据交换回内存的时间,总传输时间可能是半秒,这是一个巨大的延迟,因此,有些操作系统已经不再使用交换了。

    Segmentation(分段)

    分段是一种将内存分解为逻辑片段的技术,其中每个片段代表一组相关信息。 例如,将每个进程按照堆栈,堆,数据以及代码分为不同的段,还有OS内核的数据段等。

    将内存分解成较小的段会增加寻找空闲内存的机会。

    每个段都有一对寄存器:

    • 基址寄存器:包含段驻留在内存中的起始物理地址
    • 限制寄存器:指定段的长度

    **段表(Segment table)**存储每个段的基址和限制寄存器信息。

    使用分段时,虚拟内存地址是一对:<段号,偏移量>

    • 段号(Segment Number):用作段表的索引以查找特定条目
    • 偏移量(Offset):首先与限制寄存器进行比较,然后与基址结合以计算物理内存地址

    Paging(分页)

    有时可用内存被分成许多小块,其中没有一块足够大以满足下一个内存需求,然而他们的总和却可以。这个问题被称为碎片(Fragmentation),许多内存分配策略都会受其影响。

    分页是一种内存管理技术,它允许进程的物理内存不连续。它通过在称为页面(Page)的相同大小的块中分配内存来消除碎片问题,是目前比较优秀的内存管理技术。

    分页将物理内存划分为多个大小相等的块,称为帧(Frame)。并将进程的逻辑内存空间也划分为大小相等的块,称为页面(Page)

    任何进程中的任何页面都可以放入任何可用的帧中。

    **页表(Page Table)**用于查找此刻存储特定页面的帧。

    使用分页时,虚拟内存地址是一对:<页码,偏移量>

    • 页码(Page Number):用作页表的索引,以查找此页面的条目
    • 偏移量(Offset):与基址相结合,以定义物理内存地址

    举一个分页地址转换的例子:

    虚拟内存地址为0x13325328,页表项0x13325包含的值是0x03004,那么物理地址是什么?

    答案:
    物理地址是0x03004328
    页码为0x13325,偏移量为0x328
    相应的帧号是0x03004

    Linux的内存管理机制

    在Linux系统下,监控内存常用的命令是free、top等,下面是一个free命令的执行结果:

    要了解Linux的内存管理,首先要明白上例中各个名词的意义:

    • total:物理内存的总大小。
    • used:已经使用的物理内存多小。
    • free:空闲的物理内存值。
    • shared:多个进程共享的内存值。
    • buffers / cached:用于磁盘缓存的大小(这部分是从物理内存中划出来的)。
    • 第二行Mem:代表物理内存使用情况。
    • 第三行(-/+ buffers/cached):代表磁盘缓存使用状态。
    • 第四行:Swap表示交换空间内存使用状态(这部分实际上是从磁盘上虚拟出来的逻辑内存)。

    free命令输出的内存状态,可以从两个角度来看:内核角度、应用层角度。

    1.从内核角度来查看内存的状态:

    就是内核目前可以直接分配到,不需要额外的操作,即free命令第二行 Mem 的输出。从上例中可见,41940 + 16360492 = 16402432,也就是说Mem行的 free + used = total,注意,这里的free并不包括buffers和cached。

    2.从应用层角度来查看内存的状态:

    也就是Linux上运行的程序可以使用的内存大小,即free命令第三行 -/+ buffers/cache 的输出。再来做一个计算41940+(465404+12714880)=13222224,即Mem行的free + buffers + cached = -/+ buffers/cache行的free,也就是说应用可用的物理内存值是Mem行的free、buffers和cached三者之和,可见-/+ buffers/cache行的free是包括buffers和cached的。

    对于应用程序来说,buffers/cached占有的内存是可用的,因为buffers/cached是为了提高文件读取的性能,当应用程序需要用到内存的时候,buffers/cached会很快地被回收,以供应用程序使用。

    物理内存和虚拟内存

    物理内存就是系统硬件提供的内存大小,是真正的内存。在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为 交换空间(Swap Space)

    linux的内存管理采取的是分页存取机制,为了保证物理内存能得到充分的利用,内核会在适当的时候将物理内存中不经常使用的数据块自动交换到虚拟内存中,而将经常使用的信息保留到物理内存。而进行这种交换所遵循的依据是“LRU”算法(Least Recently Used,最近最少使用算法)。

    Buffers和Cached有什么用

    在任何系统中,文件的读写都是一个耗时的操作,当应用程序需要读写文件中的数据时,操作系统先分配一些内存,将数据从磁盘读入到这些内存中,然后应用程序读写这部分内存数据,之后系统再将数据从内存写到磁盘上。如果是大量文件读写甚至重复读写,系统读写性能就变得非常低下,在这种情况下,Linux引入了缓存机制。

    buffers与cached都是从物理内存中分离出来的,主要用于实现磁盘缓存,用来保存系统曾经打开过的文件以及文件属性信息,这样当操作系统需要读取某些文件时,会首先在buffers与cached内存区查找,如果找到,直接读出传送给应用程序,否则,才从磁盘读取,通过这种缓存机制,大大降低了对磁盘的IO操作,提高了操作系统的数据访问性能。而这种磁盘高速缓存则是基于两个事实:第一,内存访问速度远远高于磁盘访问速度;第二,数据一旦被访问,就很有可能短期内再次被访问。

    另外,buffers与cached缓存的内容也是不同的。buffers是用来缓冲块设备做的,它只记录文件系统的元数据(metadata)以及 tracking in-flight pages,而cached是用来给文件做缓冲。更通俗一点说:buffers主要用来存放目录里面有什么内容,文件的属性以及权限等等。而cached直接用来记忆我们打开过的文件和程序

    为了验证我们的结论是否正确,可以通过vi打开一个非常大的文件,看看cached的变化,然后再次vi这个文件,感觉一下是不是第二次打开的速度明显快于第一次?

    接着执行下面的命令:

    find /* -name  *.conf
    

    看看buffers的值是否变化,然后重复执行find命令,看看两次显示速度有何不同。

    Linux内存管理的哲学

    Free memory is wasted memory.

    Linux的哲学是尽可能多的使用内存,减少磁盘IO,因为内存的速度比磁盘快得多。Linux总是在力求缓存更多的数据和信息,内存不够时,将一些不经常使用的数据转移到交换分区(Swap Space)中以释放更多可用物理内存,当然,如果交换分区的数据再次被读写时,又会被转移到物理内存中,这种设计思路提高了系统的整体性能。而Windows的处理方式是,内存和虚拟内存一起使用,不是以内存操作为主,结果就是IO的负担比较大,可能拖慢处理速度。

    Linux和Windows在内存管理机制上的区别

    在Linux系统使用过程中,你会发现,无论你的电脑内存配置多么优越,仍然不时的发生可用内存吃紧的现象,感觉内存不够用了,其实不然。这是Linux内存管理的优秀特性,无论物理内存有多大,Linux都将其充分利用,将一些程序调用过的硬盘数据缓存到内存,利用内存读写的高速性提高系统的数据访问性能。而Window只在需要内存时,才为应用分配内存,不能充分利用大容量的内存空间。换句话说,每增加一些内存,Linux都能将其利用起来,充分发挥硬件投资带来的好处,而Windows只将其作为摆设。

    所以说,一般我们不需要太关注Linux的内存占用情况,而如果Swap占用率一直居高不下的话,就很有可能真的是需要扩展内存了

    Android的内存管理机制

    Android使用虚拟内存和分页,不支持交换

    垃圾收集

    无论是ART还是Dalvik虚拟机,都和众多Java虚拟机一样,属于一种托管内存环境(程序员不需要显示的管理内存的分配与回收,交由系统自动管理)。托管内存环境会跟踪每个内存分配, 一旦确定程序不再使用一块内存,它就会将其释放回堆中,而无需程序员的任何干预。 回收托管内存环境中未使用内存的机制称为垃圾回收

    垃圾收集有两个目标:

    • 在程序中查找将来无法访问的数据对象;
    • 回收这些对象使用的资源。

    Android的垃圾收集器不带压缩整理功能(Compact),即不会对Heap做碎片整理。

    Android的内存堆是分代式(Generational)的,意味着它会将所有分配的对象进行分代,然后分代跟踪这些对象。 例如,最近分配的对象属于年轻代(Young Generation)。 当一个对象长时间保持活动状态时,它可以被提升为年老代(Older Generation),之后还能进一步提升为永久代(Permanent Generation)

    每一代的对象可占用的内存总量都有其专用上限。 每当一代开始填满时,系统就会执行垃圾收集事件以试图释放内存。 垃圾收集的持续时间取决于它在收集哪一代的对象以及每一代中有多少活动对象

    虽然垃圾收集速度非常快,但它仍然会影响应用程序的性能。通常情况下你不需要控制代码中何时执行垃圾收集事件。 系统有一组用于确定何时执行垃圾收集的标准。 满足条件后,系统将停止执行当前进程并开始垃圾回收。 如果在像动画或音乐播放这样的密集处理循环中发生垃圾收集,则会增加处理时间。 这种增加可能会导致你的应用程序中的代码执行超过建议的16ms阈值。

    为实现高效,流畅的帧渲染,Android建议绘制一帧的时间不要超过16ms。

    此外,你的代码可能会执行各种工作,这些工作会导致垃圾收集事件更频繁地发生,或使其持续时间超过正常范围。 例如,如果在Alpha混合动画的每个帧期间在for循环的最内部分配多个对象,则大量的对象就会污染内存堆。 此时,垃圾收集器会执行多个垃圾收集事件,并可能降低应用程序的性能。

    共享内存

    Android可以跨进程共享RAM页面(Pages)。 它可以通过以下方式实现:

    • 每个应用程序进程都是从名为Zygote的现有进程分叉(fork)出来的。 Zygote进程在系统引导并加载framework代码和资源(例如Activity Themes)时启动。 要启动新的应用程序进程,系统会fork Zygote进程,然后在新进程中加载并运行应用程序的代码。 这种方法允许在所有应用程序进程中共享大多数的为framework代码和资源分配的RAM页面

    • 大多数静态数据都被映射到一个进程中。 该技术允许在进程之间共享数据,并且还允许在需要时将其Page out。这些静态数据包括:Dalvik代码(通过将其置于预链接的.odex文件中进行直接的memory-mapping),app资源(通过将资源表设计为可以mmap的结构并通过对齐APK的zip条目) 和传统的项目元素,如.so文件中的本地代码。
    • 在许多地方,Android使用显式分配的共享内存区域(使用ashmem或gralloc)在进程间共享相同的动态RAM。 例如,Window surface在应用程序和屏幕合成器之间使用共享内存,而游标缓冲区在Content Provider和客户端之间使用共享内存。

    分配和回收应用的内存

    Android为每个进程分配内存的时候,采用了弹性分配方式,也就是刚开始并不会一下分配很多内存给每个进程,而是给每一个进程分配一个“够用”的虚拟内存范围。这个范围是根据每一个设备实际的物理内存大小来决定的,并且可以随着应用后续需求而增加,但最多也只能达到系统为每个应用定义的上限。

    堆的逻辑大小与其使用的物理内存总量并不完全相同。 在检查应用程序的堆时,Android会计算一个名为“比例集大小”(PSS)的值,该值会考虑与其他进程共享的脏页面和干净页面,但其总量与共享该RAM的应用程序数量成正比。 此PSS总量就是系统认为是你的物理内存占用量。

    Android会在内存中尽量长时间的保持应用进程,即使有些进程不再使用了。这样,当用户下次启动应用的时候,只需要恢复当前进程就可以了,不需要重新创建进程,进而减少应用的启动时间。只有当Android系统发现内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android就会决定关闭某些进程以回收内存。关于这部分内容,稍后再细说。

    限制应用的内存

    为了维护高效的多任务环境,Android为每个应用程序设置了堆大小的硬性限制。 该限制因设备而异,取决于设备总体可用的RAM。 如果应用程序已达到该限制并尝试分配更多内存,则会收到 OutOfMemoryError

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

    切换应用

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

    如果你的应用程序具有缓存进程并且它保留了当前不需要的内存,那么即使用户未使用它,你的应用程序也会影响系统的整体性能。 当系统内存不足时,就会从最近最少使用的进程开始,终止LRU Cache中的进程。另外,系统还会综合考虑保留了最多内存的进程,并可能终止它们以释放RAM。

    当系统开始终止LRU Cache中的进程时,它主要是自下而上的。 系统还会考虑哪些进程占用更多内存,因为在它被杀时会为系统提供更多内存增益。 因此在整个LRU列表中消耗的内存越少,保留在列表中并且能够快速恢复的机会就越大。

    Android对Linux系统的内存管理机制进行的优化

    Android对内存的使用方式同样是“尽最大限度的使用”,这一点继承了Linux的优点。只不过有所不同的是,Linux侧重于尽可能多的缓存磁盘数据以降低磁盘IO进而提高系统的数据访问性能,而Android侧重于尽可能多的缓存进程以提高应用启动和切换速度。Linux系统在进程活动停止后就结束该进程,而Android系统则会在内存中尽量长时间的保持应用进程,直到系统需要更多内存为止。这些保留在内存中的进程,通常情况下不会影响系统整体运行速度,反而会在用户再次激活这些进程时,加快进程的启动速度,因为不用重新加载界面资源了,这是Android标榜的特性之一。所以,Android现在不推荐显式的“退出”应用

    那为什么内存少的时候运行大型程序会慢呢,原因是:在内存剩余不多时打开大型程序会触发系统自身的进程调度策略,这是十分消耗系统资源的操作,特别是在一个程序频繁向系统申请内存的时候。这种情况下系统并不会关闭所有打开的进程,而是选择性关闭,频繁的调度自然会拖慢系统。

    Android中的进程管理

    说到Android的内存管理,就不得不提到进程管理,因为进程管理确确切切的影响着系统内存。在了解进程管理之前,我们首先了解一些基础概念。

    当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。 如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。 但是,你也可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

    Android应用模型的设计思想取自Web 2.0的Mashup概念,是基于组件的应用设计模式。在该模型下,每个应用都由一系列的组件搭建而成,组件通过应用的配置文件描述功能。Android系统依照组件的配置信息,了解各个组件的功能并进行统一调度。这就意味着,来自不同应用的组件可以有机地结合在一起,共同完成任务,各个Android应用,只有明确的组件边界,而不再有明确的进程边界和应用边界。这种设计,也令得开发者无需耗费精力去重新开发一些附属功能,而是可以全身心地投入到核心功能的开发中。这样不但提高了应用开发的效率,也增强了用户体验(比如电子邮件中选择图片作为附件的功能,可以直接调用专门的图片应用的功能,不用自己从头开发)。

    系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行(四大组件的各个生命周期回调方法都是在UI线程中触发的)。

    进程的生命周期

    Android的一个不寻常的基本特征是应用程序进程的生命周期并非是由应用本身直接控制的。相反,进程的生命周期是由系统决定的,系统会权衡每个进程对用户的相对重要程度,以及系统的可用内存总量来确定。比如说相对于终止一个托管了正在与用户交互的Activity的进程,系统更可能终止一个托管了屏幕上不再可见的Activity的进程,否则这种后果是可怕的。因此,是否终止某个进程取决于该进程中所运行组件的状态。Android会有限清理那些已经不再使用的进程,以保证最小的副作用。

    作为应用开发者,了解各个应用组件(特别是Activity、Service和BroadcastReceiver)如何影响应用进程的生命周期非常重要。不正确的使用这些组件,有可能导致系统在应用执行重要工作时终止进程。

    举个常见的例子, BroadcastReceiver 在其 onReceive() 方法中接收到Intent时启动一个线程,然后从该函数返回。而一旦返回,系统就认为该 BroadcastReceiver 不再处于活动状态,因此也就不再需要其托管进程(除非该进程中还有其他组件处于活动状态)。这样一来,系统就有可能随时终止进程以回收内存,而这也最终会导致运行在进程中的线程被终止。此问题的解决方案通常是从 BroadcastReceiver 中安排一个 JobService ,以便系统知道在该进程中仍有活动的工作。

    为了确定在内存不足时终止哪些进程,Android会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。必要时,系统会首先杀死重要性最低的进程,以此类推,以回收系统资源。这就相当于为进程分配了优先级的概念。

    进程优先级

    Android中总共有5个进程优先级(按重要性降序):

    Foreground Process:前台进程(正常不会被杀死)

    用户当前操作所必需的进程。有很多组件能以不同的方式使得其所在进程被判定为前台进程。如果一个进程满足以下任一条件,即视为前台进程:

    • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
    • 托管某个 Service,后者绑定到用户正在交互的 Activity
    • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
    • 托管正执行其 onReceive() 方法的 BroadcastReceiver

    通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

    Visible Process:可见进程(正常不会被杀死)

    没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。杀死这类进程也会明显影响用户体验。 如果一个进程满足以下任一条件,即视为可见进程:

    • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,启动了一个对话框样式的前台 activity ,此时在其后面仍然可以看到前一个Activity。

      运行时权限对话框就属于此类。
      考虑一下,还有哪种情况会导致只触发onPause而不触发onStop?

    • 托管通过 Service.startForeground() 启动的前台Service。

      Service.startForeground():它要求系统将它视为用户可察觉到的服务,或者基本上对用户是可见的。

    • 托管系统用于某个用户可察觉的特定功能的Service,比如动态壁纸、输入法服务等等。

    可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。如果这类进程被杀死,从用户的角度看,这意味着当前 activity 背后的可见 activity 会被黑屏代替。

    Service Process:服务进程(正常不会被杀死)

    正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,后台网络上传或下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

    已经运行很久(例如30分钟或更久)的Service,有可能被降级,这样一来它们所在的进程就可以被放入Cached LRU列表中。这有助于避免一些长时间运行的Service由于内存泄漏或其他问题而消耗过多的RAM,进而导致系统无法有效使用缓存进程的情况。

    Background / Cached Process:后台进程(可能随时被杀死)

    这类进程一般会持有一个或多个目前对用户不可见的 Activity (已调用 Activity 的 onStop() 方法)。它们不是当前所必须的,因此当其他更高优先级的进程需要内存时,系统可能随时终止它们以回收内存。但如果正确实现了Activity的生命周期,即便系统终止了进程,当用户再次返回应用时也不会影响用户体验:关联Activity在新的进程中被重新创建时可以恢复之前保存的状态。

    在一个正常运行的系统中,缓存进程是内存管理中唯一涉及到的进程:一个运行良好的系统将始终具有多个缓存进程(为了更高效的切换应用),并根据需要定期终止最旧的进程。只有在非常严重(并且不可取)的情况下,系统才会到达这样一个点,此时所有的缓存进程都已被终止,并且必须开始终止服务进程。

    Android系统回收后台进程的参考条件
    LRU算法:自下而上开始终止,先回收最老的进程。越老的进程近期内被用户再次使用的几率越低。杀死的进程越老,对用户体验的影响就越小。
    回收收益:系统总是倾向于杀死一个能回收更多内存的进程,因为在它被杀时会为系统提供更多内存增益,从而可以杀死更少的进程。杀死的进程越少,对用户体验的影响就越小。换句话说,应用进程在整个LRU列表中消耗的内存越少,保留在列表中并且能够快速恢复的机会就越大。

    这类进程会被保存在一个伪LRU列表中,系统会优先杀死处于列表尾部(最老)的进程,以确保包含用户最近查看的 Activity 的进程最后一个被终止。这个LRU列表排序的确切策略是平台的实现细节,但通常情况下,相对于其他类型的进程,系统会优先尝试保留更有用的进程(比如托管用户主应用程序的进程,或者托管用户看到的最后一个Activity的进程,等等)。还有其他一些用于终止进程的策略:对允许的进程数量硬限制,对进程可以持续缓存的时间量的硬限制,等等。

    在一个健康的系统中,只有缓存进程或者空进程会被系统随时终止,如果服务进程,或者更高优先级的可见进程以及前台进程也开始被系统终止(不包括应用本身糟糕的内存使用导致OOM),那就说明系统运行已经处于一个亚健康甚至极不健康的状态,可用内存已经吃紧。

    Empty Process:空进程(可以随时杀死)

    不含任何活跃组件的进程。保留这种进程的的唯一目的是用作缓存(为了更加有效的使用内存而不是完全释放掉),以缩短下次启动应用程序所需的时间,因为启动一个新的进程也是需要代价的。只要有需要,Android会随时杀死这些进程。

    内存管理中对于前台/后台应用的定义,与用于Service限制目的的后台应用定义不同。从Android 8.0开始,出于节省系统资源、优化用户体验、提高电池续航能力的考量,系统进行了前台/后台应用的区分,对于后台service进行了一些限制。在该定义中,如果满足以下任意条件,应用将被视为处于前台:

    • 具有可见 Activity(不管该 Activity 已启动还是已暂停)。
    • 具有前台 Service。
    • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个 Service,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的 Service,那么该应用处于前台:
      IME
      壁纸 Service
      通知侦听器
      语音或文本 Service
      如果以上条件均不满足,应用将被视为处于后台。详见后台Service限制

    Android系统如何评定进程的优先级

    根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的最高级别。例如,如果某进程同时托管着 Service 和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

    此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进程其级别永远不会低于其所服务的进程。 例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

    由于运行服务的进程其级别高于托管后台 Activity 的进程,因此,在 Activity 中启动一个长时间运行的操作时,最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,一个文件上传的操作就可以考虑使用服务来完成,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。 同理, BroadcastReceiver 也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

    Home键退出和返回键退出的区别

    Home键退出,程序保留状态为后台进程;而返回键退出,程序保留状态为空进程,空进程更容易被系统回收。Home键其实主要用于进程间切换,返回键则是真正的退出程序。

    从理论上来讲,无论是哪种情况,在没有任何后台工作线程(即便应用处于后台,工作线程仍然可以执行)的前提下,被置于后台的进程都只是保留他们的运行状态,并不会占用CPU资源,所以也不耗电。只有音乐播放软件之类的应用需要在后台运行Service,而Service是需要占用CPU时间的,此时才会耗电。所以说没有带后台服务的应用是不耗电也不占用CPU时间的,没必要关闭,这种设计本身就是Android的优势之一,可以让应用下次启动时更快。然而现实是,很多应用多多少少都会有一些后台工作线程,这可能是开发人员经验不足导致(比如线程未关闭或者循环发送的Handler消息未停止),也可能是为了需求而有意为之,导致整个Android应用的生态环境并不是一片干净。

    作为用户,你需要手动管理内存吗?

    你有“内存使用率过高”恐慌症吗?

    无论是使用桌面操作系统还是移动操作系统,很多人都喜欢随时关注内存,一旦发现内存使用率过高就难受,忍不住的要杀进程以释放内存。这种习惯很大程度上都是源自Windows系统,当然这在Windows下也确实没错。然而很多人在使用Linux系统时仍然有这个习惯,甚至到了Android系统下,也改不掉(尤其是Android手机刚出现的几年),Clean Master等各种清理软件铺天盖地。毫不客气的说,Windows毒害了不少人!当然,这也不能怪Windows,毕竟Windows的普及率太高了,而大部分普通用户(甚至一些计算机相关人员)又不了解Windows和Linux在内存管理方面的差别。

    何时需要清理手机的RAM?

    考虑到许多手机厂商都内置了“清理”功能,那这个东西可能也有些道理。事实上,关闭应用程序以节省内存的做法,仅在少数情况下是值得尝试的 —— 当应用崩溃或无法正常运行时。比如以下情况:

    • 你的微信在启动时需要加载很久
    • 某个应用启动时闪退或者运行过程中闪退
    • 系统响应速度非常缓慢

    这些症状可能非常多样化,甚至原因不明的手机发热也可能是由于某个崩溃的应用造成的。

    管理你的手机RAM:结论

    究竟需不需要手动清空内存?答案是:No!

    清空内存意味着你需要不断重启应用,这需要花费时间和电量,甚至会缩短电池寿命。内存占用高其实并非是一件坏事,甚至是需要的。因为Android是基于Linux内核的操作系统,而Linux的内存哲学是:

    Free memory is wasted memory.

    你只需要在手机明显变慢时采取行动。一般来说,系统的自动RAM管理才是最快最高效的,也是Android标榜的优势之一。关闭应用可能释放一些内存,但却对高效使用内存毫无作用。

    Leave the memory management to Android, and it will leave the fun to you.

    参考资料

    浅谈Linux的内存管理机制
    Processes and Threads Overview
    Process and Application Lifecycle
    Overview of Memory Management
    RAM management on Android: why you shouldn’t clear memory

    展开全文
  • 内存管理我的理解是分为两个部分,一个是物理内存的管理,另一个部分是物理内存地址到虚拟地址的转换。 物理内存管理 内核中实现了很多机制和算法来进行物理内存的管理,比如大名鼎鼎的伙伴系统,以及slab分配器等等...

    内存管理我的理解是分为两个部分,一个是物理内存的管理,另一个部分是物理内存地址到虚拟地址的转换。

    物理内存管理

    内核中实现了很多机制和算法来进行物理内存的管理,比如大名鼎鼎的伙伴系统,以及slab分配器等等。我们知道随着Linux系统的运行,内存是不断的趋于碎片化的,内存碎片分为两种类型,一种为外碎片,所谓外碎片就是以页为单位的内存之间的碎片化,另一种为内碎片,内碎片是指同一个页面内的碎片化,那么如果来优化这种内存碎片问题呢?

    • 伙伴系统
      伙伴系统可以用来减少外碎片的,通过更加合理的分配以页为单位的内存,可以减少外碎片的产生,以尽量保持系统内存的连续性。

    • slab分配器
      slab是用于优化内碎片问题的,通过把小块内存以对象的方式管理起来,并且创建slab缓存,方便同种类型对象的分配和释放,减少了内碎片的产生,同时这些小块内存会尽可能的保持在硬件cache中,所以极大提升了访问效率。

    物理内存管理这块比较复杂,本文仅仅做一个简述,关于这两者的内核API以及实现,将在后续文章中再做介绍。

    物理地址到虚拟地址的转换

    本文只介绍内核中访问所有物理内存的方式,当前我们面对的问题是:如何物理内存映射到内核空间(3G-4G)这一段区域内?对于用户空间访问物理内存的话题,我们后续再开专门的文章进行介绍。
    内核中把物理内存的低端区域作为直接映射区,高地址区域定义为高端内存,通过一个变量high_memory来界定他们的分界线。high_memory是一个虚拟地址,定义了高端内存被允许映射到内核的起始地址。
    它在arm平台上的定义如下:

    void * high_memory;
    
    EXPORT_SYMBOL(high_memory);
    
    
    arm_lowmem_limit = lowmem_limit;
    
    high_memory = __va(arm_lowmem_limit - 1) + 1;
    
    if (!memblock_limit)
        memblock_limit = arm_lowmem_limit;
    
    

    以我的测试板子为例:

    Memory: 1030548K/1048576K available (5078K kernel code, 221K rwdata, 1624K rodata, 1584K init, 179K bss, 18028K reserved, 0K cma-reserved, 270336K highmem)
    Virtual kernel memory layout:
        vector  : 0xffff0000 - 0xffff1000   (   4 kB)
        fixmap  : 0xffc00000 - 0xfff00000   (3072 kB)
        vmalloc : 0xf0000000 - 0xff000000   ( 240 MB)
        lowmem  : 0xc0000000 - 0xef800000   ( 760 MB)
        pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
        modules : 0xbf000000 - 0xbfe00000   (  14 MB)
          .text : 0xc0008000 - 0xc0693dd8   (6704 kB)
          .init : 0xc0694000 - 0xc0820000   (1584 kB)
          .data : 0xc0820000 - 0xc0857708   ( 222 kB)
           .bss : 0xc0857708 - 0xc0884700   ( 180 kB)
    
    

    它的虚拟内存分布如上所示。这块信息的实现代码如下:

         pr_notice("Virtual kernel memory layout:\n"
                 "    vector  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
     #ifdef CONFIG_HAVE_TCM
                 "    DTCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
                 "    ITCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
     #endif
                 "    fixmap  : 0x%08lx - 0x%08lx   (%4ld kB)\n",
                 MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
                     (PAGE_SIZE)),
     #ifdef CONFIG_HAVE_TCM
                 MLK(DTCM_OFFSET, (unsigned long) dtcm_end),
                 MLK(ITCM_OFFSET, (unsigned long) itcm_end),
     #endif
                 MLK(FIXADDR_START, FIXADDR_END));
     #ifdef CONFIG_ENABLE_VMALLOC_SAVING
         print_vmalloc_lowmem_info();
     #else
         printk(KERN_NOTICE
                "    vmalloc : 0x%08lx - 0x%08lx   (%4ld MB)\n"
                "    lowmem  : 0x%08lx - 0x%08lx   (%4ld MB)\n",
                 MLM(VMALLOC_START, VMALLOC_END),
                 MLM(PAGE_OFFSET, (unsigned long)high_memory));
     #endif
         printk(KERN_NOTICE
     #ifdef CONFIG_HIGHMEM
                "    pkmap   : 0x%08lx - 0x%08lx   (%4ld MB)\n"
     #endif
     #ifdef CONFIG_MODULES
                "    modules : 0x%08lx - 0x%08lx   (%4ld MB)\n"
     #endif
                "      .text : 0x%p" " - 0x%p" "   (%4d kB)\n"
                "      .init : 0x%p" " - 0x%p" "   (%4d kB)\n"
                "      .data : 0x%p" " - 0x%p" "   (%4d kB)\n"
                "       .bss : 0x%p" " - 0x%p" "   (%4d kB)\n",
     #ifdef CONFIG_HIGHMEM
                 MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
                     (PAGE_SIZE)),
     #endif
     #ifdef CONFIG_MODULES
                 MLM(MODULES_VADDR, MODULES_END),
     #endif
     
                 MLK_ROUNDUP(_text, _etext),
                 MLK_ROUNDUP(__init_begin, __init_end),
                 MLK_ROUNDUP(_sdata, _edata),
                 MLK_ROUNDUP(__bss_start, __bss_stop));
     
    
    

    我们通过上面机器的启动log打印出来的memory layout可以知道,在3G以下的区域也是被内核数据所占用了,可是上面不是说用户空间是0-3G吗?这里不会被用户所占用导致冲突吗?
    实际上,用户空间的映射区定义如下:

    00001000    TASK_SIZE-1 User space mappings
                    Per-thread mappings are placed here via
                    the mmap() system call.
    
    

    这里TASK_SIZE实际上并不是PAGE_OFFSET-1,而是中间间隔了一段区域(16M):

    /*
     * TASK_SIZE - the maximum size of a user space task.
     * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area
     */
    #define TASK_SIZE       (UL(CONFIG_PAGE_OFFSET) - UL(SZ_16M))
    #define TASK_UNMAPPED_BASE  ALIGN(TASK_SIZE / 3, SZ_16M)
    
    

    低端内存映射

    内核空间1G的虚拟空间,其中有一部分用于直接映射,线性映射区,在arm32平台上,物理地址[0:760M]这部分内存被线性的映射到[3G:3G+760M]的虚拟地址上,剩余的264M虚拟地址做什么呢?
    是保留给高端内存映射使用的,这部分是能够动态分配和释放的,因为平台上实际的物理内存可能会超过1G,那么内核必须要具有能够寻址到整个物理内存的能力。线性映射区在启动时就完成了页表的创建,没有必要再过多介绍。
    测试平台上的线性映射区域:

     lowmem  : 0xc0000000 - 0xef800000   ( 760 MB)
    

    对应的解释如下:

     PAGE_OFFSET high_memory-1   Kernel direct-mapped RAM region.
                     This maps the platforms RAM, and typically
                     maps all platform RAM in a 1:1 relationship.
    

    高端内存映射

    针对高端内存的映射,内核又划分了多个区域,因为需要在264M有限的区域内去访问除了760M之外的所有物理内存,所以这部分相比线性映射区将变得更加复杂。内核有三种方式用于将高端内存映射到内核空间,分别是pkmap、fixmap和vmalloc。

    • pkmap

    测试平台上的数据如下:

     pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
    

    说明:

     PKMAP_BASE  PAGE_OFFSET-1   Permanent kernel mappings
                     One way of mapping HIGHMEM pages into kernel
                     space.
    

    永久内核映射区,映射高端内存到内核空间的一种方式。pkmap在用于映射高端物理内存的,当我们从伙伴系统中分配到高端内存后,是无法直接操作的,必须要经过map操作,此时就可以使用pkmap,它对应的
    内核API为

     void *kmap(struct page *page);
    

    传入的是一个物理内存页对应的struct page结构体,返回一个虚拟地址。使用方法如下:
    使用alloc_pages()在高端存储器区得到struct page结构,然后调用kmap(struct *page)在内核地址空间[PKMAP_BASE : PAGE_OFFSET-1]中建立永久映射,如果page结构对应的是低端物理内存的页,该函数仅仅返回该页对应的虚拟地址。
    另外需要注意kmap()可能引起睡眠,所以不能用在中断和持有锁的代码中使用。从使用方法上我们知道,它是针对struct page来进行的操作,所以至少会映射一个page。

    • fixmap
      测试平台上的数据如下:
    fixmap  : 0xffc00000 - 0xfff00000   (3072 kB)
    

    说明:

    ffc00000    ffefffff    Fixmap mapping region.  Addresses provided
                    by fix_to_virt() will be located here.
    
    

    fixmap也叫临时映射区,他是一个固定的一块虚拟空间用于映射不同的物理地址,并且是在申请时使用,不用时释放。它于pkmap的区别在于这块地址的映射不会引起睡眠,是可以在中断和持有锁的代码中运行的,它的内核API如下:

    void *kmap_atomic(struct page *page);
    
    • vmalloc

    测试平台上的数据如下:

     vmalloc : 0xf0000000 - 0xff000000   ( 240 MB)
     lowmem  : 0xc0000000 - 0xef800000   ( 760 MB)
    

    说明:

     VMALLOC_START   VMALLOC_END-1   vmalloc() / ioremap() space.
                     Memory returned by vmalloc/ioremap will
                     be dynamically placed in this region.
                     Machine specific static mappings are also
                     located here through iotable_init().
                     VMALLOC_START is based upon the value
                     of the high_memory variable, and VMALLOC_END
                     is equal to 0xff800000.
    

    从平台打印的数据来看,vmalloc和lowmem线性映射区并没有完全紧靠着,而是中间有一个hole空洞(8M),这个8M的空间是为了捕获越界访问的。
    vmalloc会分配非连续物理内存,这里的非连续指的是物理内存不连续,虚拟地址是连续的,优先使用高端内存来分配物理页,如果分配失败,才会从Normal zone分配。这个接口和上面的都不同,它会自动分配物理内存,然后完成映射后直接返回虚拟地址,而上面两个都是只进行映射。

    void *vmalloc(unsigned long size);
    
    展开全文
  • STM32内存管理

    千次阅读 2019-03-15 15:28:43
    内存管理详解 1、介绍 内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。 内存管理的实现方法有很多种,他们其实最终都是...
  • C++内存管理(面试版)

    千次阅读 多人点赞 2020-03-29 13:43:42
    C++的内存管理 一、C++内存管理详解 1、内存的分配方式 (a)(a)(a)栈:编译器分配的内存,用来存储函数的局部变量,函数调用结合素则自动释放内存。 (b)(b)(b)堆:程序员用new分配的内存,一般存储指针;如果程序运行...
  • Linux内存管理(最透彻的一篇)

    万次阅读 多人点赞 2019-05-16 14:27:22
    【转】Linux内存管理(最透彻的一篇) 摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友...
  • 鸿蒙虚拟内存全景图 图来自鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】 再看鸿蒙用户空间全景图 图来自鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】 以上两图是笔者...
  • 为了便于内存的分配和释放,AWorks提供了两种内存管理工具:堆和内存池。 本文为《面向AWorks框架和接口的编程(上)》第三部分软件篇——第9章内存管理——第1~2小节:堆管理器和内存池。 本章导...
  • Linux内存管理机制(最透彻的一篇)

    万次阅读 多人点赞 2018-08-05 14:10:09
    摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友分析Linux的内存管理与使用。在本章最后,...
  • 内存管理:页式虚拟内存管理

    千次阅读 2019-12-17 21:00:38
    页式存储管理——虚拟内存——缺页中断,页面替换算法 开章明意: 创建一个进程(创建进程是在磁盘中),进程以字节为单位编号,然后再进程分为许多页(每页4KB),内存中有对应的页框(设定同页)。通过页表(记录...
  • unity 内存管理

    千次阅读 2018-08-17 14:19:42
    转载至以下文章: https://onevcat.com/2012/11/memory-in-unity3d/ Unity 3D中的内存管理 https://docs.unity3d.com/Manual/iphone-playerSizeOptimization.html iOS stripping level(官网) ...
  • Flink内存管理源码解读之内存管理

    千次阅读 2016-04-06 22:55:54
    回顾上一篇文章我们谈了Flink自主内存管理的一些基础的数据结构。那篇中主要讲了数据结构的定义,这篇我们来看看那些数据结构的使用,以及内存的管理设计。概述这篇文章我们主要探讨Flink的内存管理类MemoryManager...
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 ...在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换
  • Spark内存管理之堆内/堆外内存原理详解

    千次阅读 多人点赞 2019-09-27 10:47:06
    文章目录概要1. 堆内内存(On-heap Memory)1.1 堆内内存分区2. 堆外内存(Off-heap Memory)3. Execution 内存和 Storage 内存动态调整3.1 动态...介绍Spark内存管理中,涉及到的**堆内内存(On-heap Memory)和堆外内...
  • C语言的内存管理机制

    万次阅读 多人点赞 2018-07-22 15:39:30
    本文是作者在学习C语言内存管理的过程中做的一个总结。 变量概念: 全局变量(外部变量):出现在代码块{}之外的变量就是全局变量。 局部变量(自动变量):一般情况下,代码块{}内部定义的变量就是自动变量,...
  • 内存管理算法实现

    千次阅读 2016-11-03 21:08:45
    在上一节,我们得知可用内存的大小后,我们就可以开发一个简单的管理算法去管理和分配可用用内存
  • 浅谈c++内存管理

    万次阅读 2018-12-25 17:00:48
    1.从内存对齐讲起 对于结构体变量内存对齐遵循以下三个原则: 1.变量的起始地址能够被其对齐值整除,结构体变量的对齐值为最宽的成员大小。 2.结构体每个成员相对于起始地址的偏移能够被其自身对齐值整除,如果不...
  • Linux虚拟内存管理

    千次阅读 多人点赞 2017-05-22 21:21:15
    Linux的虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址空间并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正的物理地址 如果虚拟地址所对应...
  • 一篇文章搞清spark内存管理

    千次阅读 2020-03-27 15:02:38
    内存空间分配2.1 早期的静态内存管理2.2 统一内存管理机制3. 存储内存(Storage)缓存RDD怎么存储4. 执行内存(Execution)怎么管理 ​ 在执行Spark的应用程序时,Spark集群会启动Driver和Executor两种JVM进程,前者为...
  • 这是第三篇文章,我们将会讨论一个由于日常使用的编程语言日益成熟和复杂度提升从而让开发者忽略的话题——内存管理。我们还将提供一些有关如何处理 SessionStack 中 JavaScript 内存泄漏的建议,因为我们需要确保 ...
  • C++-内存管理

    千次阅读 2019-06-18 11:05:24
    **通过学习,这不是开玩笑,在我所学习的语言中,对内存的使用有如此的执着的“较真”也就是C++了。 因为性能的缘故,一个内存不合理的分配,都可能在日后成为一个隐患,成为一个令人头疼的BUG。所...
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-08-31 Linux-4.7 X86 & arm ... Linux内存管理 http://blog.csdn.net/vanbreaker/article/details/75799411 前景回顾前面我们讲到服务器体系(SMP, NUMA, M
  • malloc内存管理总结

    千次阅读 2018-08-14 13:03:49
    内存管理 内存管理主要包含两个层面的内容: 1、操作系统内核相关的内存管理:物理内存层 2、库函数层:主要是堆内存,即malloc实现层 如果用户还有需要会在用户层再做一次内存管理机制,例如SGI STL中的...
  • 内存管理

    万次阅读 2015-03-12 17:09:40
    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...
  • 1.前言 在执行Spark的应用程序时,Spark集群会启动Driver和Executor两种JVM进程,前者为...由于Driver的内存管理相对来说较为简单,本文主要对Executor的内存管理进行分析,下文中的Spark内存均特指Executor的内存。
  • MySQL内存管理,内存分配器和操作系统

    万次阅读 多人点赞 2019-09-10 17:00:00
    另一个改善内存管理的因素是cpu内核数量:在测试中,cpu核数越多,内存返回给操作系统的速度会越快。这可能是你拥有多个CPU,而其中一个可专门用作内存分配器释放内存给操作系统。 The very first implementation of...
  • 计算机操作系统_内存管理

    千次阅读 2018-12-17 02:27:01
    内存管理 设计程序模拟内存的动态分区内存管理方法。内存空闲区使用空闲分区表进行管理,采用最先适应算法从空闲分区表中寻找空闲区进行分配,内存回收时不考虑与相邻空闲区的合并。 假定系统的内存共640K,初始...
  • 页目录,页表2.Windows内存管理3.CPU段式内存管理4.CPU页式内存管理 一、基本概念1. 两个内存概念物理内存:人尽皆知,就是插在主板上的内存条。他是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外...
  • Linux内存描述之内存页面page--Linux内存管理(四)

    万次阅读 多人点赞 2016-08-31 14:18:44
    日期 内核版本 架构 作者 GitHub CSDN 2016-08-31 ... Linux内存管理 1 前景回顾1.1 UMA和NUMA两种模型共享存储型多处理机有两种模型 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 非均匀存储器

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,509,013
精华内容 603,605
关键字:

内存管理