精华内容
下载资源
问答
  • 内存管理的概念及作用

    千次阅读 2018-05-25 13:34:26
    操作系统对内存的划分和动态分配,就是内存管理的概念。有效的内存管理在多道程序设计中非常重要,不仅方便用户使用存储器、提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器。内存管理的功能:1.内存空间的...

    操作系统对内存的划分和动态分配,就是内存管理的概念。有效的内存管理在多道程序设计中非常重要,不仅方便用户使用存储器、提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器。

    内存管理的功能:

    1.内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率

    2.地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址转换功能,把逻辑地址转换成相应的物理地址

    3.内存空间的扩充:利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存

    4.存储保护:保证各道作业在各自的存储空间内运行,互不干扰

    展开全文
  • Flink内存管理源码解读之内存管理器

    千次阅读 2016-04-06 22:55:54
    回顾上一篇文章我们谈了Flink自主内存管理的一些基础的数据结构。那篇中主要讲了数据结构的定义,这篇我们来看看那些数据结构的使用,以及内存的管理设计。概述这篇文章我们主要探讨Flink的内存管理类MemoryManager...

    回顾

    上一篇文章我们谈了Flink自主内存管理的一些基础的数据结构。那篇中主要讲了数据结构的定义,这篇我们来看看那些数据结构的使用,以及内存的管理设计。

    概述

    这篇文章我们主要探讨Flink的内存管理类MemoryManager涉及到对内存的分配、回收,以及针对预分配内存而提供的memory segment pool。还有支持跨越多个memory segment数据访问的page view。

    本文探讨的类主要位于pageckage : org.apache.flink.runtime.memory下。完整类图:

    flink-source-code-analysis-memory-management-1_all-class-diagram

    MemoryManager

    MemoryManager 作为Flink的内存管理器,承担着MemorySegment的分配、回收等职责。

    为了提升MemorySegment的复用能力,它提供了不同memory type的MemorySegment池的实现,它们是:

    • HeapMemoryPool
    • HybirdOffHeapMemoryPool

    首先,这里为了提升memory segment操作效率,MemoryManager鼓励长度相等的memory segment。由此引入了page的概念。其实page跟memory segment没有本质上的区别,只不过是为了体现memory segment被分配为均等大小的内存空间而引入的。可以将这个类比于操作系统的页式内存分配,page这里看着同等大小的block即可。MemoryManager提供的默认page size为32KB,并提供了自定义page size的下界值不得小于4KB。

        /** The default memory page size. Currently set to 32 KiBytes. */
        public static final int DEFAULT_PAGE_SIZE = 32 * 1024;
    
        /** The minimal memory page size. Currently set to 4 KiBytes. */
        public static final int MIN_PAGE_SIZE = 4 * 1024;

    MemoryManager允许自定义page size,它提供的构造器可以指定这个参数,一个MemoryManager应对一个page size,而且指定了就不允许改变。

    page化的segment跟非page化的segment:

    flink-source-code-analysis-memory-management-1_page-and-non-page

    将segment page化会给后面的跨多个segment的访问带来更高的效率。

    MemoryManager这个类本身没有特别的地方,并可能会被跨线程共享。这时某些操作可能会牵扯到多线程的并发问题。因此,MemoryManager提供了一个对象作为锁,以在某些方法上进行同步操作。

        /** The lock used on the shared structures. */
        private final Object lock = new Object();

    两个构造器:

        public MemoryManager(long memorySize, int numberOfSlots) {
        public MemoryManager(long memorySize, int numberOfSlots, int pageSize, MemoryType memoryType, boolean preAllocateMemory) {

    第一个构造器内部调用了第二个构造器,并用DEFAULT_PAGE_SIZE作为pageSize,HEAP作为memory type。

    需要提及一下的是,这里的参数numberOfSlots 是跟Flink的task manager相关,暂时不做过多介绍,等到我们讲解Flink runtime时再细说。

    第二个构造器的另一个参数preAllocateMemory,指定memory manager的内存分配策略是预分配还是按需分配。我们后面会看到,对于这两种策略,相关的内存申请和释放操作是不同的。

    第二个构造器内就已经根据memory type将特定的memory pool对象初始化好了:

            switch (memoryType) {
                case HEAP:
                    this.memoryPool = new HeapMemoryPool(memToAllocate, pageSize);
                    break;
                case OFF_HEAP:
                    this.memoryPool = new HybridOffHeapMemoryPool(memToAllocate, pageSize);
                    break;
                default:
                    throw new IllegalArgumentException("unrecognized memory type: " + memoryType);
            }

    通过定位到两个pool对象的构造器,可以看到在实例化构造器的时候就已经将需要预分配的内存分配到位了(当然,这里是针对preAllocateMemory为true的调用情景而言),因为如果该参数为false,那么pool构造器的memToAllocate将会被置为0。

            public HeapMemoryPool(int numInitialSegments, int segmentSize) {
                this.availableMemory = new ArrayDeque<byte[]>(numInitialSegments);
                this.segmentSize = segmentSize;
    
                for (int i = 0; i < numInitialSegments; i++) {
                    this.availableMemory.add(new byte[segmentSize]);
                }
            }
            public HybridOffHeapMemoryPool(int numInitialSegments, int segmentSize) {
                this.availableMemory = new ArrayDeque<ByteBuffer>(numInitialSegments);
                this.segmentSize = segmentSize;
    
                for (int i = 0; i < numInitialSegments; i++) {
                    this.availableMemory.add(ByteBuffer.allocateDirect(segmentSize));
                }
            }

    针对offheap的内存池对象,分配内存调用的是java SE的API

    public static ByteBuffer allocateDirect(int capacity);

    该API用于分配一个新的direct byte buffer,大小限制即为capacity,其他的一切都是不确定的(包括它内部是否是一个byte数组)。

    MemoryManager并没有类似于open这样的方法,它的构造器已经包含了这些动作。但它有明确的shutdown方法,主要的作用是:释放内存,清空内存池。

        public void shutdown() {
            // -------------------- BEGIN CRITICAL SECTION -------------------
            synchronized (lock)
            {
                if (!isShutDown) {
                    // mark as shutdown and release memory
                    isShutDown = true;
                    numNonAllocatedPages = 0;
    
                    // go over all allocated segments and release them
                    for (Set<MemorySegment> segments : allocatedSegments.values()) {
                        for (MemorySegment seg : segments) {
                            seg.free();
                        }
                    }
    
                    memoryPool.clear();
                }
            }
            // -------------------- END CRITICAL SECTION -------------------
        }

    下面进入到allocate以及release方法,这里因为篇幅的关系,我们只关注核心逻辑。这两个方法都共同拥有一个参数owner,说白了就是一个映射关系,谁申请的memory segment,将会挂到谁的名下,释放的时候也从谁的名下删除。大致如下图所示:

    flink-source-code-analysis-memory-management-1_allocated-segment

    allocate:

            synchronized (lock)
            {
    
                Set<MemorySegment> segmentsForOwner = allocatedSegments.get(owner);
                if (segmentsForOwner == null) {
                    segmentsForOwner = new HashSet<MemorySegment>(numPages);
                    allocatedSegments.put(owner, segmentsForOwner);
                }
    
                if (isPreAllocated) {
                    for (int i = numPages; i > 0; i--) {
                        MemorySegment segment = memoryPool.requestSegmentFromPool(owner);
                        target.add(segment);
                        segmentsForOwner.add(segment);
                    }
                }
                else {
                    for (int i = numPages; i > 0; i--) {
                        MemorySegment segment = memoryPool.allocateNewSegment(owner);
                        target.add(segment);
                        segmentsForOwner.add(segment);
                    }
                    numNonAllocatedPages -= numPages;
                }
            }

    上面的预防性检查就不看了,主要逻辑有三段。先获取该owner下的segment集合,如何不存在,则初始化;然后如果是预分配的模式,则直接从池里取出来,加入到当前owner申请的segment集合中;如果不是预分配模式,则立即分配。

    release分为单个memory segment释放以及多segment一起释放。

    先来看单个memory segment释放:

    synchronized (lock)
            {
                // remove the reference in the map for the owner
                try {
                    Set<MemorySegment> segsForOwner = this.allocatedSegments.get(owner);
    
                    if (segsForOwner != null) {
                        segsForOwner.remove(segment);
                        if (segsForOwner.isEmpty()) {
                            this.allocatedSegments.remove(owner);
                        }
                    }
    
                    if (isPreAllocated) {
                        // release the memory in any case
                        memoryPool.returnSegmentToPool(segment);
                    }
                    else {
                        segment.free();
                        numNonAllocatedPages++;
                    }
                }
                catch (Throwable t) {
                    throw new RuntimeException("Error removing book-keeping reference to allocated memory segment.", t);
                }
            }

    基本和allocate是相反的逻辑,不过有一点要注意的是,如果当前释放的segment是segsForOwner集合中的最后一个,那么将segsForOwner也从allocatedSegments中移除。

    释放多个memory segment:

            synchronized (lock)
            {
                // since concurrent modifications to the collection
                // can disturb the release, we need to try potentially multiple times
                boolean successfullyReleased = false;
                do {
                    final Iterator<MemorySegment> segmentsIterator = segments.iterator();
    
                    Object lastOwner = null;
                    Set<MemorySegment> segsForOwner = null;
    
                    try {
                        // go over all segments
                        while (segmentsIterator.hasNext()) {
    
                            final MemorySegment seg = segmentsIterator.next();
                            if (seg == null || seg.isFreed()) {
                                continue;
                            }
    
                            final Object owner = seg.getOwner();
    
                            try {
                                // get the list of segments by this owner only if it is a different owner than for
                                // the previous one (or it is the first segment)
                                if (lastOwner != owner) {
                                    lastOwner = owner;
                                    segsForOwner = this.allocatedSegments.get(owner);
                                }
    
                                // remove the segment from the list
                                if (segsForOwner != null) {
                                    segsForOwner.remove(seg);
                                    if (segsForOwner.isEmpty()) {
                                        this.allocatedSegments.remove(owner);
                                    }
                                }
    
                                if (isPreAllocated) {
                                    memoryPool.returnSegmentToPool(seg);
                                }
                                else {
                                    seg.free();
                                    numNonAllocatedPages++;
                                }
                            }
                            catch (Throwable t) {
                                throw new RuntimeException(
                                        "Error removing book-keeping reference to allocated memory segment.", t);
                            }
                        }
    
                        segments.clear();
    
                        // the only way to exit the loop
                        successfullyReleased = true;
                    }
                    catch (ConcurrentModificationException e) {
                        // this may happen in the case where an asynchronous
                        // call releases the memory. fall through the loop and try again
                    }
                } while (!successfullyReleased);
            }

    可以看到,核心逻辑跟单个segment的释放并没有太大的不同,但最外层套了个do/while循环,用于在释放失败之后不断重试,直到释放成功为止。这里可能涉及到对正在释放的segments的并发修改产生ConcurrentModificationException异常而失败。

    还有一个基于owner的释放方法:

        public void releaseAll(Object owner) {

    逻辑也是大同小异,不再废话。

    以上,MemoryManager的绝大部分方法都介绍完毕,逻辑相对还是比较清晰的。下面简单介绍一下几个内部的Pool,上面也已经提过了。

    MemoryPool

    MemoryPool是真正跟MemorySegment打交道的地方,涉及到申请、分配、回收等操作。它提供了两个实现类,两个pool的实现都采用JDK的ArrayDeque(双端队列)作为存储所有内存块引用的数据结构。

    对于内存块的类型,HeapMemoryPool所代表的on-heap内存使用byte[]表示,而HybridOffHeapMemoryPool所代表的off-heap内存使用ByteBuffer表示。

    注意这里的分类方式不同于我们讲基础数据结构时的HeapMemorySegement以及HybirdMemorySegment划分方式,这里为了表示内存块的引用,直接区分了on-heap和off-heap,没有所谓的hybird memory segment。

    关于pool对象,没有太多特别的,主要是三个接口方法:

    • allocateNewSegment : 直接申请一个新的segment,该segment不属于pool
    • requestSegmentFromPool : 从池里请求一个segment,调用ArrayDeque的remove(首元素出队)方法,获得一个内存块引用,并包装成segment
    • returnSegementToPool : 调用ArrayDeque的add(入队到尾部)方法,添加内存块引用并调用MemorySegmentfree方法释放该segement

    AbstractPagedXXXView

    还记得我们在谈Flink内存管理基础数据结构时谈到DataInputViewDataOutputView。这个包就提供了基于page的对view的进一步实现(并非完整实现,从名字也能看出,它只是个抽象类)。

    说得更直白一点就是,它提供了跨越多个memory page的数据访问(input/output)视图。它包含了从page中读取/写入数据的解码/编码方法以及跨越page的边界检查(边界检查主要由实现类来完成)。

    说说两个View类都有的几个字段:

    • currentSegment : 表示当前正在操作的memory segment
    • headerLength : 每个memory segment前面有一段是头部,可能存储一些元数据信息,数据访问的时候需要跳过这个长度,要求这个pageview指代的所有memory segment的header length都相等
    • positionInSegment : 类似于一个指针,指向某个segment的某个位置(相对segment的位置)

    AbstractPagedInputView

    • limitInSegment : 指定跟下一个segment的界限值

    AbstractPagedOutputView

    • segmentSize : 指定所有segment size的值,也即一个page view里所有的segment长度都是一样的。

    两个view类都有一些共同的方法,其中的一个是advance,它用于从当前segment切换到下一个segment。

    其他的一些方法都是数据输入/输出相关的特定方法。

    我们看一个最核心的方法,AbstractPagedOutputView的write方法:

        public void write(byte[] b, int off, int len) throws IOException {
            int remaining = this.segmentSize - this.positionInSegment;
            if (remaining >= len) {
                this.currentSegment.put(this.positionInSegment, b, off, len);
                this.positionInSegment += len;
            }
            else {
                if (remaining == 0) {
                    advance();
                    remaining = this.segmentSize - this.positionInSegment;
                }
                while (true) {
                    int toPut = Math.min(remaining, len);
                    this.currentSegment.put(this.positionInSegment, b, off, toPut);
                    off += toPut;
                    len -= toPut;
    
                    if (len > 0) {
                        this.positionInSegment = this.segmentSize;
                        advance();
                        remaining = this.segmentSize - this.positionInSegment;  
                    }
                    else {
                        this.positionInSegment += toPut;
                        break;
                    }
                }
            }
        }

    这就是一个跨越多个segment的写数据的核心方法,我们大致分析一下逻辑。

    前提说明:remaining这个变量始终表示,currentSegment剩余的内存。

    首先,判断remaining跟len的大小关系,如果

    remaining >= len

    即剩余空间是足够写入len长度的数据的,那么就直接写入数据,将positionInSegment后移len长度

    否则,表明currentSegment剩余空间不够。首先判断一个临界点,如果没有剩余空间了:

    remaining == 0

    则直接切换到下一个segment,并重新计算remaining。然后进入一个循环:判断,切换segment,计算剩余空间,移动指针。大致是:

    remaininglen之间取较小值,作为待写入的长度。然后将toPut长度的数据写入currentSegemnt。然后off指针后移toPut长度(注意,off是指向待写入数据b数组的);同时写入的数据长度len减去已写入的toPut长度。

    接着判断,如果是remainng 小于 len:

    len > 0

    那么说明还是要切换到下一个segment(currentSegment 不够存)。然后重新计算新的currentSegment重新进入循环流程。

    否则,说明remainng大于len,说明数据已经足够存储了,则移动positionInSegment指针,然后跳出循环。

    那么AbstractPagedInputView的read方法也就不难理解了的。

    总结

    本文分析了Flink的内存管理机制。总得来说有几点是其核心思路:

    • 构建segment对象池(pool化),增强复用性,减少重复分配,回收的开销
    • 规范segment的大小(page化),提升操作效率
    • 抽象跨segment的访问复杂性(view化)

    apache_flink_weichat

    展开全文
  • 分页内存管理

    千次阅读 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

    展开全文
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 ...在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换
  • MMU内存管理单元详解

    千次阅读 2020-04-28 23:15:07
    在传统的批处理系统如 DOS 系统,应用程序与操作系统在内存中的布局大致如下图: 应用程序直接访问物理内存,操作系统占用一部分内存区。 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。...
  • 内存管理:程序是如何被优雅的装载到内存中的

    千次阅读 多人点赞 2021-11-04 09:26:35
    内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入到主板上的,因此对于一台计算机...带着这些疑问我们来看看计算系统内存管理那些事。 内存的交换技术..
  • C语言的内存管理机制

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

    万次阅读 多人点赞 2019-05-16 14:27:22
    【转】Linux内存管理(最透彻的一篇) 摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友...
  • windows虚拟内存管理

    千次阅读 2016-07-21 09:22:16
    内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理。在应用程序中我们无时不刻不在和...
  • FreeRTOS之内存管理详解

    千次阅读 2019-06-23 10:21:58
    Freertos内核源码解读之--------内存管理 内存管理 任务栈和系统栈的区别 FreeRTOS内存管理方法 FreeRTOS静态创建 一、内存管理 在c语言中定义了4个区:代码区、全局变量和静态变量区、动态变量区(即...
  • [内存管理]连续内存分配(CMA)概述

    千次阅读 2013-10-21 09:00:06
    连续内存分配(CMA - Contiguous Memory...这个框架的主要作用不是分配内存,而是解析和管理内存配置,以及作为在设备驱动程序和可插拔的分配之间的中间组件。因此,它是与任何内存分配方法和分配策略没有依赖关系的
  • Android内存管理机制

    千次阅读 2018-06-29 12:29:42
    解读 Andriod 内存管理机制 一、进程类型 1、前台进程(foreground):目前正在屏幕上显示的进程和一些系统进程。举例来说, Dialer Storage,Google Search
  • 这是第三篇文章,我们将会讨论一个由于日常使用的编程语言日益成熟和复杂度提升从而让开发者忽略的话题——内存管理。我们还将提供一些有关如何处理 SessionStack 中 JavaScript 内存泄漏的建议,因为我们需要确保 ...
  • 在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据。... 本文为《面向AWorks框架和接口的编程(上)》第三部分软件篇——第9章内存管理——第1~2小节:堆管理和内存池。 本章导...
  • C++内存管理(面试版)

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

    千次阅读 2016-06-06 20:28:53
    而且了解了Java的内存管理,有助于优化JVM,从而使得自己的应用获得最佳的性能体验。所以还等什么,赶紧跟着我来一起学习这方面的知识吧~ Java内存管理分为两个方面:内存分配和垃圾回收,下面我们一一的来看一下。 ...
  • 操作系统-内存管理

    万次阅读 多人点赞 2019-06-06 09:20:53
    文章目录一、内存管理1.1 内存的基础知识1.1.1 什么是内存,有何作用1.1.2 进程运行的原理-指令1.1.3 逻辑地址VS物理地址1.1.4 进程运行的基本原理(从写程序到程序运行)1.1.5 装入内存的三种方式1.1.5 链接的三种...
  • malloc内存管理总结

    千次阅读 2018-08-14 13:03:49
    如果用户还有需要会在用户层再做一次内存管理机制,例如SGI STL中的内存管理机制(二级配置)。 由于篇幅有限,本文主要介绍库函数层的malloc实现机制。同时上述两层中由于操作系统等不同也存在差异,例如malloc...
  • Linux内存管理机制(最透彻的一篇)

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

    千次阅读 2015-12-22 17:58:20
    提到C语言,我们知道C语言和其他高级语言的...因此,我们要对C语言中的内存管理,有个系统的了解。  在C语言中,定义了4个内存区间:代码区;全局变量和静态变量区;局部变量区即栈区;动态存储区,即堆区;具体如下:
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-08-31 ... Linux内存管理 1 前景回顾前面我们讲到服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)1.1 UMA和NUMA两种模型共享存储型多处理机有两种模型
  • Linux内存管理机制

    千次阅读 2017-02-16 04:07:24
    这是Linux内存管理的一个优秀特性,主要特点是,无论物理内存有多大,Linux 都将其充份利用,将一些程序调用过的硬盘数据读入内存(buffer/cache),利用内存读写的高速特性来提高Linux系统的数据访问性能。...
  • 计算机操作系统_内存管理

    千次阅读 2018-12-17 02:27:01
    内存管理 设计程序模拟内存的动态分区内存管理方法。内存空闲区使用空闲分区表进行管理,采用最先适应算法从空闲分区表中寻找空闲区进行分配,内存回收时不考虑与相邻空闲区的合并。 假定系统的内存共640K,初始...
  • C/C++内存分配管理

    万次阅读 多人点赞 2018-08-13 14:57:23
    内存分配及管理 1.内存分配方式 在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。 堆:堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言...
  • 任务管理器内存列的含义

    千次阅读 2016-08-31 21:12:49
    任务管理器内存列的含义是什么? 在“任务管理器”中,可以通过将列添加到“进程”选项卡上所显示的信息中,来监视计算机上正在运行的进程。这些列将显示有关每个进程的信息,如进程当前正在使用的中央处理 ...
  • 鸿蒙虚拟内存全景图 图来自鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】 再看鸿蒙用户空间全景图 图来自鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】 以上两图是笔者...
  • iOS内存管理:基本概念与原理

    千次阅读 2017-02-13 19:09:52
    在Objective-C的内存管理中,其实就是引用计数(reference count)的管理。内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员对内存资源使用不当,有时不仅会造成内存资源浪费,...
  • 页目录,页表2.Windows内存管理3.CPU段式内存管理4.CPU页式内存管理 一、基本概念1. 两个内存概念物理内存:人尽皆知,就是插在主板上的内存条。他是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外...
  • Java内存管理-内存分配与回收

    千次阅读 2017-12-29 17:48:00
    Java内存管理-内存分配与回收

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 555,324
精华内容 222,129
关键字:

内存管理器的作用