内存优化之重新认识内存

news2024/12/27 20:02:20

我们知道,手机的内存是有限的,如果应用内存占用过大,轻则引起卡顿,重则导致应用崩溃或被系统强制杀掉,更严重的情况下会影响应用的留存率。因此,内存优化是性能优化中非常重要的一部分。但是,很多开发者对内存的认识还停留在应用开发这一层,平时只是参考网上的方案,对内存进行比较浅显的优化。想要深入进行内存优化,我们需要从操作系统的层面了解内存是怎么管理的,又是如何被使用的

可能会有人疑问:“为什么做个内存优化需要从操作系统层了解内存呢?”我们确实可以在网上搜到很多内存优化的文章,但它们都是从上层应用出发进行优化的,而不同的应用因为环境不一样、业务不一样,很多优化方法都不能通用。因此,只有当我们从底层掌握了内存的原理,从下而上地制定优化方案,才能适用于任何业务,甚至当我们转型到 iOS、前端或者后端都能通用。

接下来,我们就从操作系统底层出发,重新认识内存。

我们先将目光放到操作系统的早期,在这个环境下,程序都是直接操作物理内存的。比如一个程序执行如下指令:

MOV REGISTER1,0

计算机会将位置为 0 的物理内存中的内容移到 REGISTER1 的寄存器中。在这种情况下,如果第二个程序在 0 的位置写入一个新的值,就会擦掉第一个程序存放在相同位置上的所有内容,导致第一个程序崩溃。

正因为应用程序可以直接操作物理内存,所以我们完全可以修改其他程序在内存中的数据,导致程序崩溃或者产生安全问题。因此,对当时的操作系统来说,同时运行多个程序很困难。

为了解决这个问题,我们自然而然会想到:不允许应用程序直接操作物理内存。于是虚拟内存的技术诞生了。

为了更好地了解什么是虚拟内存,我们先看看早期直接操作物理内存系统下的内存模型长什么样。从下面内存模型的简化图中我们可以看到,物理内存中存在两块数据,一个是操作系统的数据,一个是应用程序的数据。除此之外,其实还会有设备驱动程序的数据,它们不是我们了解的重点就先不列上去了。

image.png

什么是虚拟内存?

虚拟内存技术相当于给每个程序一个独占且连续的内存,比如 32 位系统下是 4G(2^32),只不过这个内存是虚拟的。同时,虚拟内存需要能够映射到真实的物理内存。简化的内存模型如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-haHe4vHL-1670148923898)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/edd00859a2094eaa964f8ce588df1f18~tplv-k3u1fbpfcp-watermark.image?)]
从上图的简化内存模型,我们可以看到,每个程序都可以独享一块虚拟内存。在 Linux 系统上,一个进程代表着一个程序,这里我们可以理解成每个进程都独享一块虚拟内存,这块虚拟内存在 32 位系统下是 4G(2^32),64 位系统下是 2^48,即 256TB(这里不是 2^64,是因为 256TB 已经足够大了,如果用 2^64,会有大量的寻址空间浪费)。

其次,虚拟内存都由应用程序和操作系统这两部分组成。其中,应用程序这部分虚拟内存是应用独占的,操作系统这部分虚拟内存则由所有进程共享,而所有进程的操作系统这部分虚拟内存都指向了同一段物理内存。虚拟内存到物理内存的映射由操作系统来实现,操作系统在做映射操作时会寻找可用的物理内存,不可能出现覆盖其他数据的情况,这让同时运行多个程序成为了可能。

上图的虚拟内存是一个简化的模型。实际上,虚拟内存和物理内存都是按照页来管理和映射的,一页的大小为 4KB,我们来看一个 32 位 Android 系统、物理内存为 2G 的设备的内存模型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ysAU5ah2-1670148923899)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efb87e2370624ccdb09642d2018c0581~tplv-k3u1fbpfcp-watermark.image?)]

可以看到,物理内存和虚拟内存是通过 4K 大小的页一一对应的。这个时候,如果我们再用文章最开头的那个指令:

MOV REGISTER1,0

在虚拟内存技术的加持下,此时计算机就不会直接将物理地址为 0 的内存移到 REGISTER1 寄存器了,而是先寻找虚拟地址 0 对应的物理地址 4096,然后将物理地址为 4096 的内容移到 REGISTER1 寄存器中

MOV REGISTER1,4096

虚拟地址转换成物理地址是由计算机的内存管理单元(MMU)完成的,它属于硬件部分而不是系统软件部分,所以转换速度很快。

虚拟内存的内存模型

知道了虚拟内存由操作系统和应用程序两部分组成,并且虚拟内存都由页来维护和管理之后,我们再深入了解一下 Linux 系统中虚拟内存的内存模型,它需要和 Linux 系统的可执行文件,也就是 ELF 文件一起配合来看。

image.png

为了让你理解起来更简单,这里的 ELF 文件格式也被我简化了,后面用到的时候再进行深入介绍。在 Linux 系统中,存放操作系统的虚拟内存区域被称为内核空间,剩下的存放应用的虚拟内存区域称为用户空间。 内核空间占用了 1G,位于虚拟地址的高地址区域,而 ELF 文件的一些数据,是存放在低地址的区域(即从地址 0 开始)。下面我详细解释一下内存模型中的几个区域

  1. 栈:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。

  2. 堆:动态内存分配,可以由开发者自己分配和释放(malloc 和 free 函数实现),Android 开发时不需要我们手动分配和释放,因为虚拟机程序已经帮我们做了。堆的开始地址由变量 start_brk 描述,堆的当前地址由变量 brk 描述。

  3. BSS:存放全局未初始化,静态未初始化数据。

  4. 数据段:存放全局初始化,静态初始化数据。

  5. 程序代码区:存放的是 ELF 文件代码段。

可以看到,栈内存的分配是从上到下的,而堆内存的分配是从下到上的,这种方式可以最大程度利用虚拟内存的空间。

虚拟内存分配

前面我们已经知道,应用是没法直接操作物理内存的,所以我们在开发 App 时分配的内存实际都是虚拟内存。那么我们怎么申请虚拟内存呢?

开发 Android 应用时,并不需要我们自己去分配内存,直接 new 一个对象,声明一个变量或者常量即可,也不需要我们自己去做释放,但所有的数据都需要内存,这些都是虚拟机帮我们做。虚拟机分配申请内存主要使用的是 malloc() 函数,它是 C 语言库的一个标准函数。

void *malloc(size_t size)

malloc 函数是一个 C 语言库的函数,所以它分配内存最终还是得调用 Linux 系统提供的函数,让 Linux 内核去帮我们申请一块内存。内核会调用 mmap() 函数,在堆中分配我们想要的内存空间大小。 mmap() 函数是 Linux 系统一个很重要的函数,我们需要深刻认识它。

void *mmap(void *addr,size_t length,int prot,int flags,int fd, off_t offset);
  • 参数 addr 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址;
  • 参数 length 表示将文件中多大的部分映射到内存;
  • 参数 prot 指定映射区域的读写权限;
  • 参数 flags 指定映射时的特性,如是否允许其他进程映射这段内存;
  • 参数 fd 指定映射内存的文件描述符;
  • 参数 offset 指定映射位置的偏移量,一般为 0。

mmap 函数有 2 种用法:

  1. 映射磁盘文件到用户空间中;
  1. 匿名映射,不映射磁盘文件,而是向映射区申请一块内存,此时的 fd 入参传 -1。

第 1 种用法可以让我们读文件的效率更高(比如 Android 读取 dex 文件就是通过 mmap 来提高读取速度),也可以用来实现数据跨进程传输(比如 Android 共享内存机制、Binder 通信都是通过 mmap 来实现的)。malloc() 函数使用了 mmap 函数的第 2 种用法,即在 Heap 区域中申请一块内存。

需要注意的,这里申请的内存都是虚拟内存,并且这个时候并不会分配真正的物理内存,只有当我们真正要往这块虚拟内存区域写入数据时,操作系统检查到对应的虚拟内存没有映射到物理内存,便会发生缺页中断,然后分配一块同样大小的物理内存,并建立映射关系。这是一种懒加载技术,也是内存优化的方案之一。

malloc() 函数在申请内存小于 128k 时会使用 sbrk() 函数,sbrk() 会将堆顶指针(即前面提到的 brk)向高地址移动,获得新的虚存空间,这些策略都是基于性能考虑的。比如 Android 虚拟机在分配大对象时,也会专门放在 LargeObjectSpcace 中,这些就不展开讲了。至于 Linux 系统是如何发生缺页中断,如何分配物理内存,如何建立映射关系的,都属于 Linux 系统相关知识了,更详细的知识点会在后面的篇章中结合实战项目穿插着讲解。

小结

事实上,直接操作物理内存的操作系统并没有消失,我们现在的嵌入式设备,如冰箱,微波炉等等都能直接操作物理内存。这其实也符合它们的使用场景,直接操作物理内存会让性能开销更小,操作也更方便。但需要同时运行多个软件的系统都有虚拟内存,可以说虚拟内存是现代操作系统最重要的发明之一了。

当我们重新认识了内存后,我们再来看内存优化,它其实分为两部分。

  • 一是物理内存的优化:也就是这个程序实际消耗的物理内存。
  • 二是虚拟内存的优化:在前面我们也知道了 32 位机只有 3G 的虚拟内存可用,所以一个比较大的Android 程序,很容易就会出现虚拟内存不足的情况(64 位系统就完全不用担心这个问题)。

在后面的章节中,我会针对这两部分,总结出体系的优化方法论,再搭配讲解一些优化实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/61166.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入体会线程状态的切换

✨✨hello,愿意点进来的小伙伴们,你们好呐! 🐻🐻系列专栏:【JavaEE初阶】 🐲🐲本篇内容:线程状态详解 🐯🐯作者简介:一名现大二的三非编程小白&am…

微机-------CPU与外设之间的数据传送方式

目录 一、无条件方式二、查询方式三、中断方式四、DMA方式一、无条件方式 外设要求:简单、数据变化缓慢。 外设被认为始终处于就绪状态。始终准备好数据或者始终准备好接收数据。 IN AL,数据端口 数据端口的地址通过CPU的地址总线送到地址译码器进行译码,同时该指令进行的是…

JAVASE(复习)——异常

所有的异常都是在java.lang包中的Throwable类中 一、Exception 和 Error 的区别 exception:程序本身发生的异常,可以捕获抛出异常,一般用try—catch—finally捕获。 error:发生在jvm层面的错误,程序无法处理。 二…

Git 如何调整 commit 的顺序

title: Git 如何调整 commit 的顺序 date: 2022-12-02 23:11 tags: [git] 〇、问题 使用哪条命令调整commit的顺序? git rebase -i 一、前言 今天测试了git hooks,产生了大量的commit,而后又进行了正常的commit,因此在这里是想要…

java——mybatis——Mybatis注解开发——@Update——修改数据

DAO接口: package com.sunxl.dao;import com.sunxl.pojo.User; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.SelectKey; import org.apache.ibatis.annotations.Update;impo…

SpringBoot+Thymeleaf上传头像并回显【表单提交】

参考文章:springbootthymeleaf实现图片上传并回显https://www.wanmait.com/note/shaowei/javaee/b3717a24fde24d3e89c47765a1a63214.html 一、新建SpringBoot项目 添加 spring web和 thymeleaf 的依赖 二、在templates新建页面 在页面中添加一个表单和一个文件上传…

8086,8088CPU管脚,奇偶地址体, ready信号,reset复位信号。规则字和非规则字

8086/8088均为40条引线,双列直插式封装,某些引线有多重功能,其功能转换有两种情况:一种是分时复用,一种是按组态定义。 用8088微处理器构成系统时,有两种不同的组态: 最小组态:808…

@AutoWired与@Resource

参考 : Qualifier - 搜索结果 - 知乎 Autowired和Resource的区别是什么? - 知乎 面试突击78:Autowired 和 Resource 有什么区别? - 掘金 目录 同一类型多个Bean报错问题 Resource注解 Resource的查找顺序 Resource注解实现依赖注入 Reso…

网课题库接口调用方法

网课题库接口调用方法 本平台优点: 多题库查题、独立后台、响应速度快、全网平台可查、功能最全! 1.想要给自己的公众号获得查题接口,只需要两步! 2.题库: 查题校园题库:查题校园题库后台(点…

QT对象树机制

Qt提供了对象树机制,能够自动、有效的组织和管理继承自QObject的Qt对象。 每个继承自QObject类的对象通过它的对象链表(QObjectList)来管理子类对象,当用户创建一个子对象时,其对象链表相应更新子类对象信息&#xff0…

Docker快速入门

容器Docker技术的演进 1.曾经部署应用,使用物理机部署,这可能会因为不同应用所依赖的版本号不同,不得已购买一套全新的机器,所以成本高、部署慢、资源浪费、难以迁移和拓展、可能会被限定硬件厂商。 2.之后引入了VMVare&#xff…

使用JPA和Hibernate查询分页

介绍 受到我最近给出的StackOverflow答案的启发,我决定是时候写一篇关于使用JPA和Hibernate时查询分页的文章了。 在本文中,您将了解如何使用查询分页来限制 JDBC大小并避免获取不必要的数据。ResultSet 如何在#Hibernate中使用查询分页来限制 JDBC 结…

pytorch深度学习实战lesson32

第三十二课 分布式训练 这个是15年的时候沐神在 CMU 装的一个小机群,里面有30台机器,各机群有大概60块 GPU , 60块 GPU一共花了三四万美金的样子,就是大概20万人民币。沐神表示最亏的是当年他们跑了太多深度学习的实验&#xff0c…

C语言-const char*,char const*,char *const理解

By: Ailson Jack Date: 2022.12.04 个人博客:http://www.only2fire.com/ 本文在我博客的地址是:http://www.only2fire.com/archives/150.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。…

传奇外网开服教程-GEE传奇外网全套架设教程

版本不同,所用的引擎和配置也会不同,但是架设方法都是大同小异,今天明杰给大家分享GEE引擎的外网架设教程。​ 需要准备的东西:DBC200版本,补丁,客户端,服务器,备案域名&#xff0c…

【Typora】Typora 新手入门参数配置记录

目录 写在前面 更改图片大小 更换高亮背景 更换主题 写在前面 最近发现一款记笔记的软件——Typora,极简清爽的外观一下子就把我给吸引住了,它支持Markdown 的格式记录,可以让笔记更加有条理、美观,至于 typora 的一些写作语法…

Android入门第43天-Activity与Activity间的互相传值

介绍 今天的课程会比较好玩,我们在之前的Service篇章中看到了一种putExtras和getExtras来进行activity与service间的传值。而恰恰这种传值其实也是Android里的通用传值法。它同样可以适用在activity与activity间传值。 Android中的传值 传单个值 传多个值 具体我…

Spring注解(简便地使用 Bean )

目录 0. 前置工作 1. 将 Bean 存储到容器 2. 对象注入&#xff08;对象装配&#xff09;【从容器中将对象读取出来】 0. 前置工作 创建Maven项目后&#xff0c;在pom.xml中添加Spring所必须的依赖。 <dependencies><dependency><groupId>org.springframe…

22个每个程序员都应该知道的 Git 命令

在这篇文章中&#xff0c;我写了一个快速学习 git 命令的备忘单。它将包括开发人员每天使用的命令&#xff0c;如 git add、git commit、git pull、git fetch&#xff0c;并共享其他有用的 git 命令。 我一直使用Git的一些命令&#xff0c;今天这个列表清单&#xff0c;希望也…

LC-6256. 将节点分成尽可能多的组(二分图判定+BFS)【周赛322】

6256. 将节点分成尽可能多的组 难度困难8 给你一个正整数 n &#xff0c;表示一个 无向 图中的节点数目&#xff0c;节点编号从 1 到 n 。 同时给你一个二维整数数组 edges &#xff0c;其中 edges[i] [ai, bi] 表示节点 ai 和 bi 之间有一条 双向 边。注意给定的图可能是不…