Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >iOS的内存分布探究

iOS的内存分布探究

作者头像
落影
发布于 2021-07-20 02:35:18
发布于 2021-07-20 02:35:18
1.6K00
代码可运行
举报
文章被收录于专栏:落影的专栏落影的专栏
运行总次数:0
代码可运行

前言

最近遇到一些内存相关crash,排查问题过程中产生对进程内整个地址空间分布的疑惑。搜查了一番资料,网上关于Linux进程地址空间分布的介绍比较详细,但是iOS实际运行效果的比较少。 本文基于网上相关文章,进行实际测试,探究App实际运行过程中的地址分布。

正文

32位的分布情况

32位的机器,每个进程会有4G虚拟地址空间,较高的1G是从0xC0000000到0xFFFFFFFF的内核空间(Kernel Space ),较低的3G是从0x00000000到0xBFFFFFFF用户空间(User Space )。 内核空间中存放的是内核代码和数据,用户空间中存放的是App进程的代码和数据。这里地址指的都是虚拟地址空间,由操作系统负责映射为物理地址。 把最常用的几个概念堆、栈、数据段、代码段做一个地址从大到小的排序:

  • 栈:在函数调用过程中,每个函数都会有一个相关的区域来存储函数的参数和局部变量,每次进行函数调用的时候系统都会往栈压入一个新的栈帧,在函数返回时清除。入栈和出栈的操作非常快,这个过程会用到两个寄存器:fp和sp寄存器。
  • 堆:在进程运行过程中,用于存储局部变量之外的变量。工作中常用的malloc函数、new操作符等可以从堆中申请内存。上面的栈很像数据结构中的栈,但这里的堆并不像数据结构的堆,其分配的方式是链表式,用brk()函数从操作系统批发内存,再零售给用户。
  • 数据段:通常指的段和data段,bss段内是未被初始化的静态变量,data段是在代码中已经初始化的静态变量。data段变大会导致启动速度变慢,bss段变大几乎不影响。因为bss段只需要预留位置,并没有真正的copy操作。相比data段增加的是具体的数据,bss段增加的只是数据描述信息。
  • 代码段:程序运行的机器指令,由代码编译产生。
64位的实际分布

对于一个iOS开发来说,目前大部分手机都是64位机器,还是需要对实际运行结果进行一些测试。 以下真机测试的机型是iPhone XS Max + iOS 14.5。

64位机器,进程内存地址从高到低分别是: 0xFFFF FFFF FFFF FFFF ⬇️ 内核空间 用户空间-保留区域 扩展使用区域 系统共享库 栈空间 内存映射区域(mmap) 堆空间 BSS段 DATA段 Text段 0x0000 0000 0000 0000

常见概念-堆、栈、数据段、代码段
堆和栈

用一段简单的代码,分别从堆和栈上面创建一块内存:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  char stack_address;
  UIView *heap_view_address = [[UIView alloc] init];
  NSLog(@"0x%016lx => stack 0x%016lx => heap", (long)&stack_address, (long)heap_view_address);

输出 0x16f4c5af7 => stack 0x100e0d8a0 => heap,可以大概知道栈和堆所在区域,0x16F4...是栈地址的开始,0x100E...是堆地址的开始。

数据段

bss段内是未被初始化的静态变量,data段是在代码中已经初始化的静态变量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 函数外-静态变量
static int vcStaticInt = 1024;
static int vcStaticNotInit;

// 函数内
NSLog(@"0x%lx => data 0x%lx => bss", (long)&vcStaticInt, (long)&vcStaticNotInit);

vcStaticNotInit代表bss段,最终的地址是0x100945788。 vcStaticInt代表data段,最终的地址是0x1009455f8

代码段

代码段是代码编译后的机器指令,可以用一个类来定位:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NSLog(@"class_address: 0x%lx\n", (long)[ViewController class]);

最终输出的class_address是0x100945500。

将这几个地址的大小进行排序,可以看到有: 0x16F4C 5AF7(栈地址) 0x100E0 D8A0(堆地址) 0x10094 5788(bss段) 0x10094 55F8(data段) 0x10094 5500(Text段)

系统共享库

下面是两个不同App(bundle id不一样)在同手机上的运行crash日志,对比可以发现:在dyld之前的系统库地址不一样,在dyld之后的地址都是一样的。

App中存在很多系统动态库,在启动时依赖dyld加载系统动态库到内存中。App依赖的具体系统动态库可能不同,但是都是iOS系统提供的。自然可以采用一种优化App启动速度方法:将所有的的系统依赖库按照固定的地址写在某个固定区域,这样只需保证App运行时这块内存不被使用,就能保证所有App启动时候不需要去装载所有的动态库。

内存映射区域

在栈空间的下方和堆空间的上方,有一块区域是内存映射区域。系统可以将文件的内容直接映射到内存,App可以通过mmap()方法请求将磁盘上文件的地址信息与进程用的虚拟逻辑地址进行映射。相比普通的读写文件,当App读取一个文件时有两步:先将文件从磁盘读取到物理内存,再从内核空间拷贝到用户空间。内存映射则可以减少操作系统的地址转换带来的消耗。

可以写一段mmap的代码来观察生成的地址

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)testMmap {
  NSString *imagePathStr = [[NSBundle mainBundle] pathForResource:@"abc" ofType:@"png"];
  size_t dataLength;
  void *dataPtr;
  // MapFile是自己写的mmap方法
  int errorCode = MapFile([imagePathStr cStringUsingEncoding:NSUTF8StringEncoding], &dataPtr, &dataLength);
  NSLog(@"mmapData:0x%lx, bytes_address:0x%lx, size:%d, error:%d", (long)dataPtr, (long)dataPtr, (long)dataLength, errorCode);
}

最终输出的dataLength地址是0x1026b8000,size是18432,注意到这个地址是在上面的堆和栈之间。

用户空间-保留区域

这一块没有查到相关信息,如有资料求分享。以下是实际运行的分析。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@interface TestOCObject : NSObject
@property (nonatomic, readonly, assign) char *name_buffer;
@end
@implementation TestOCObject {
  char name[102400];
}
- (char *)name_buffer {
  return name;
}
@end

- (void)testHeapSize:(int)count {
  NSMutableArray<TestOCObject *> *arr = [NSMutableArray new];
  while (true) {
    char stackSize;
    TestOCObject *obj = [[TestOCObject alloc] init];
    ++count;
    if (obj) {
      NSLog(@"%05d stack_address => 0x%lx heap_address => 0x%lx chars => 0x%lx", count, (long)&stackSize, (long)obj, (long)obj.name_buffer);
      [arr addObject:obj];
    }
    else {
      break;
    }
  }
}

当进程不断从堆空间申请内存,刚开始的时候从堆空间分配的地址是小于栈空间地址,但是随着内存不断被使用,在14700次左右的时候,堆空间分配的地址就会超过栈空间的地址。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
14703 stack_address => 0x16d751aef heap_address => 0x16d630000
14704 stack_address => 0x16d751aef heap_address => 0x16db28000

然后在17000次左右的时候,出现了一次大的地址变动:从0x1变成了0x2a开始。0x2a的地址空间是在系统共享库地址(0x1a)上方。

之所以有这样的现象,个人理解是为了兼容32位的情况。因为不管是系统共享库,还是堆、栈地址空间的大小,初始地址都是在32位的地址空间内。而后面地址从0x2a0000000开始,就已经超过了32位的地址空间,属于64位机器的地址空间。最终运行到达到63000次左右,一次是100KB,可以计算得到63000*100KB/1024/1024=6G左右的空间。

这时候产生了一个疑问:为什么32位的情况下,堆空间只有1G多空间大小?为什么64位的情况下,堆空间也只有6G多空间大小?(可以先暂停阅读,思考后见最下面分析)

思维发散

经过上面的分析,再来解析一下以前的问题:

普通对象和静态变量有哪些区别?

对象存储区域不同,普通对象一般是在栈、堆上,但是静态变量会存储在数据段,地址会有较大的差别。

对象实例和对象方法的关系?

一个OC对象的实例,其实就是一块存储数据的内存。内存中有指针,可以指向对象的类地址(代码段);访问一个对象方法其实是通过内存中的指针找到类地址,然后将对象的内存地址和调用的方法名作为参数传递。也可以用一种形象但可能不太恰当的比喻:执行一个方法就像带着原料跑到加工厂进行流水线的处理,原料就是对象的内存地址和其他传入方法的内存地址,流水线编译生成的固定机器指令。

栈空间地址从高到低增长?

前面已经提到,在函数调用过程中,会往栈压入一个新的栈帧,在函数返回时清除。 那么只需要构造一个递归调用,观察每个函数局部变量的地址即可观察到栈空间的地址变化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)testStackSize:(int)count {
  char stackSize[1024];
  NSLog(@"%05d stack_address => 0x%lx ", count, (long)&stackSize);
  if (count < 1000) {
    ++count;
    [self testStackSize:count];
  }
  else {
    NSLog(@"end");
  }

需要注意,同一个函数内,先后申请两个局部变量A和B,观察A和B的地址,并不能看出栈空间的地址变化。因为同一个函数内的局部变量可能会受到编译器的优化,导致不符合预期。所以观察不同栈帧间的局部变量地址变化更为准确。 通过上面的代码可以知道,栈空间地址确实是从高到低增长,随着递归函数的不断调用,局部变量的地址也在不断变小。在真机测试的情况下,两次运行的stackSize分别为 0x16ce86868和0x16ce86408,地址差为0x000000460, 转换成二进制4(16^2)+616=1024+96, 其中1024是申请的char数组,96则是函数递归调用的其他开销。这段递归代码运行994次会报错,由此可以计算主线程的栈空间有1MB左右。(此部分为实际运行效果推算,不同环境下可能结果各异)

堆空间地址从低到高增长?

堆空间的内存分配方式与栈空间不同,如果先后从堆上创建两个对象A和B,再对比两个对象的内存地址,那么A和B的大小应该没有直接关系。因为堆空间存在对象的创建和销毁,当对象A和B创建时,都有可能用到前面某些对象销毁时被回收的内存地址。 常说的堆空间地址从低到高增长,是Linux系统堆空间初始分配之后,扩大堆空间大小的时候,会往高地址增长。iOS实际运行过程中,有可能先申请到一个很大的内存地址,比如说下面这代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NSObject *oc_object = [[NSObject alloc] init];
TestOCObject *oc_big_object = [[TestOCObject alloc] init];
NSLog(@"oc_object_address => 0x%lx oc_big_object_address => 0x%lx", (long)oc_object, (long)oc_big_object);

TestOCObject是上文用到一个自定义OC类,当代码实际运行的时候,可以会看到输出 oc_object_address => 0x283d84cb0 oc_big_object_address => 0x1026b8000 其中oc_object的地址是0x283d84cb0,而oc_big_object的地址是0x1026b8000。 0x28开头的地址也会被用于分配内存,一般用于内存较小的情况,而内存比较大的时候仍然会从正常的堆地址空间开始。(这个不同地址取决于libsystem_malloc.dylib对申请内存大小的不同处理)

为什么32位的情况下,堆空间只有1G多空间大小?为什么64位的情况下,堆空间也只有6G多空间大小?

操作系统内存是段页式管理,App先分段再分页,页是内存管理的基本单位。(32位是4096B=4KB,64位是16KB) 当App访问虚拟内存时,操作系统会检查虚拟内存对应物理内存是否存在,如果不存在则触发一次缺页中断(Page Fault),将数据从磁盘加载到物理内存中,并建立物理内存和虚拟内存的映射。 32位机器的虚拟空间最多只有4G,其中1G还要留给内核空间,堆和栈之间能留下来的空间并不宽裕,即使加上栈空间到系统共享库之间的区域,总共也只有1G多空间。而64位的机器用于充足的虚拟地址空间,虚拟内存占用超过1G多之后,会从0x2a开始申请虚拟地址。但是由于有物理内存的限制,过大的虚拟内存占用会导致物理内存快速消耗,当物理内存被消耗完成后,就需要释放现有的内存页。所以App并不需要有非常大的虚拟内存,因为瓶颈往往出现在物理内存上面。 另外这里为什么可以创建6G的虚拟内存,这是因为测试代码申请的内存页大都没有写入操作,当内存有压力的时候,会被系统进行压缩成Compressed Memory。如果增加一个简单的写入操作,那么这个内存页就变成了脏内存,进程在1G多占用的时候就会被操作系统kill。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)testHeapSize:(int)count {
  NSMutableArray<TestOCObject *> *arr = [NSMutableArray new];
  while (true) {
    char stackSize;
    TestOCObject *obj = [[TestOCObject alloc] init];
    ++count;
    if (obj) {
      NSLog(@"%05d stack_address => 0x%lx heap_address => 0x%lx chars => 0x%lx", count, (long)&stackSize, (long)obj, (long)obj.name_buffer);
      // 增加write操作
      for (int i = 0; i < 100; ++i) {
        memcpy(obj.name_buffer + (i * 1024), "hello", 6);
      }
      [arr addObject:obj];
    }
    else {
      break;
    }
  }
}

辅助工具

objdump指令可以得到二进制分布,比如说下面的objdump -d LearnMemoryAddress

总结

本文为实际运行结果的分析,测试机型-iPhone XS Max + iOS 14.5。 实际运行结果的解析部分可能存在错误,如果发现请帮忙纠正。 知道各个地址空间的分布,能帮助我们更好理解iOS系统。在面对内存相关crash的时候,看到地址就能大概判断是属于哪一个区域,也能更加清晰具体去解析错误。

参考资料-Memory Usage Performance Guidelines

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
十问 Linux 虚拟内存管理 ( 一 )
该文章介绍了如何通过 pmap 命令查看进程的虚拟地址空间使用情况,包括起始地址、大小、实际使用内存、脏页大小、权限、偏移、设备和映射文件等。通过分析这些信息,可以更好地了解程序运行时的内存使用情况,并找出潜在的内存泄漏、内存碎片等问题。
陈福荣
2016/11/02
11.5K0
十问 Linux 虚拟内存管理 ( 一 )
通过小实验让你彻底理解VMA
作者简介: Loopers,码龄11年,喜欢研究内核基本原理 在32位机器上,总共有4G大小的虚拟地址空间,其中0-3G是给应用程序使用,3-4G是给内核使用。 在64位机器上,目前还不完全支持64位地址宽度,常见的地址长度有39(512GB)和48位(256TB),目前我使用的模拟器采用的是39位的地址宽度,这样的话用户空间和内核空间各占512GB的地址空间。 当一个应用程序在用户跑起来的时候,它内部是如何正常运行的,通过一个简单的例子详细说明下。 #include <stdio.h> #include 
刘盼
2022/09/28
8340
通过小实验让你彻底理解VMA
根据crash学习用户空间程序内存布局
在32位机器上,总共有4G大小的虚拟地址空间,其中0-3G是给应用程序使用,3-4G是给内核使用。
DragonKingZhu
2020/04/10
5220
根据crash学习用户空间程序内存布局
Linux 内存管理
      程序到运行主要经过程序(外存)编译,链接,装入(内存)。《程序如何运行:编译、链接、装》:
黄规速
2022/06/15
8.1K0
Linux 内存管理
【寻找Linux的奥秘】第七章:虚拟地址空间
之前在我们学习C语言和C++时我们知道,在我们的程序中不同类型的数据存储在不同的内存区域中,如下图所示(以32位平台为例):
code_monnkey_
2025/05/31
680
【寻找Linux的奥秘】第七章:虚拟地址空间
windows虚拟内存机制
在windows系统中个,每个进程拥有自己独立的虚拟地址空间(Virtual Address Space)。这一地址空间的大小与计算机硬件、操作系统以及应用程序都有关系。
全栈程序员站长
2022/07/20
1.3K0
windows虚拟内存机制
内存问题探微
因为这是我被问的最频繁的问题,哎呀我的程序 OOM 了怎么办,我的程序内存超过配额被 k8s 杀掉了怎么办,我的程序看起来内存占用很高正常吗?
范蠡
2020/12/15
9430
内存问题探微
Liunux内核内存管理之虚拟地址空间
虚拟内存就是在你电脑的物理内存不够用时把一部分硬盘空间作为内存来使用,这部分硬盘空间就叫作虚拟内存。
嵌入式Linux内核
2022/09/23
1.2K0
Liunux内核内存管理之虚拟地址空间
【Linux探索学习】第十六弹——进程地址空间:深入解析操作系统中的进程地址空间
https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482
GG Bond1
2024/11/26
6960
【Linux探索学习】第十六弹——进程地址空间:深入解析操作系统中的进程地址空间
【内存管理】内存布局介绍
32位操作系统的内存布局很经典,很多书籍都是以32位系统为例子去讲解的。32位的系统可访问的地址空间为4GB,用户空间为1GB ~ 3GB,内核空间为3GB ~ 4GB。
嵌入式与Linux那些事
2024/07/04
2380
【内存管理】内存布局介绍
【Linux系统编程】—— 虚拟内存与进程地址空间的管理:操作系统如何实现内存保护与高效分配
当我们在学习编程语言(如C语言)时,可能会遇到程序地址空间的概念。程序的地址空间是指在内存中为程序分配的一个虚拟地址区域,这个区域划分了代码段、数据段、堆、栈等不同的内存区域。
用户11286421
2025/01/20
1940
【Linux系统编程】—— 虚拟内存与进程地址空间的管理:操作系统如何实现内存保护与高效分配
别再说你不懂Linux内存管理了,10张图给你安排的明明白白!
对于精通 CURD 的业务同学,内存管理好像离我们很远,但这个知识点虽然冷门(估计很多人学完根本就没机会用上)但绝对是基础中的基础。
程序员小猿
2021/01/19
1.8K1
别再说你不懂Linux内存管理了,10张图给你安排的明明白白!
ARM32 内核内存布局
Linux内核在启动时会打印出内核内存空间的布局图,下面是ARM Vexpress平台打印出来的内存空间布局图:
233333
2020/05/18
1.7K0
ARM32 内核内存布局
五万字 | 深入理解Linux内存管理
作者简介: 程磊,一线码农,在某手机公司担任系统开发工程师,日常喜欢研究内核基本原理。 1.1 内存管理的意义 1.2 原始内存管理 1.3 分段内存管理 1.4 分页内存管理 1.5 内存管理的目标 1.6 Linux内存管理体系 2.1 物理内存节点 2.2 物理内存区域 2.3 物理内存页面 2.4 物理内存模型 2.5 三级区划关系 3.1 Buddy System 3.1.1 伙伴系统的内存来源 3.1.2 伙伴系统的管理数据结构 3.1.3 伙伴系统的算法逻辑 3.1.4 伙伴系统的接口 3.1
刘盼
2022/08/26
4.2K0
五万字 | 深入理解Linux内存管理
Linux虚拟地址空间布局
在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。
sunsky
2020/10/28
3.5K0
Linux虚拟地址空间布局
进程内存管理初探
随着cpu技术发展,现在大部分移动设备、PC、服务器都已经使用上64bit的CPU,但是关于Linux内核的虚拟内存管理,还停留在历史的用户态与内核态虚拟内存3:1的观念中,导致在解决一些内存问题时存在误解。
刘盼
2020/06/19
2.5K0
进程内存管理初探
关于进程虚拟内存
由于内存数据是固定的一个大数组,而操作系统往往是运行多个程序,如果这些程序都直接访问内存数组的话,就出现了以下问题:
仙士可
2022/02/18
2.1K0
关于进程虚拟内存
华中科技大学OS实验解析(Lab2)
版权归华中科技大学操作系统团队所有,下面是许可证书,本文档是对https://gitee.com/hustos/pke-doc的部分修改和解释.
用户7267083
2022/12/08
1.8K1
【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…
1. 从程序的运行结果可以看出一些端倪,就是一个全局变量在地址并未改变的情况下,竟然出现了不同的值,这说明什么呢?首先一个变量肯定是只能有一个值的,但是地址只有一个,而变量的值却出现了两个,那么就必须说明一个结论,现在在内存中应该出现了两个变量了,因为一个变量是绝对不可能出现两个值的,所以我们可以推导出的结论就是内存中现在一定出现了两个全局变量global_value。
举杯邀明月
2023/04/12
1.6K0
【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…
Linux系统面试题
用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。
thierryzhou
2022/12/01
1.7K1
Linux系统面试题
推荐阅读
相关推荐
十问 Linux 虚拟内存管理 ( 一 )
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验