当前位置: 首页 > news >正文

编译原理——运行时存储组织与内存管理

📌目录

  • 🔍 编译原理——运行时存储组织与内存管理
    • 🌐 一、运行时存储组织概述
      • 🔧 (一)运行时存储组织的作用与任务
      • 📦 (二)程序运行时存储空间的布局
      • 🎯 (三)存储分配策略
    • 🧩 二、活动记录:函数调用的幕后账本
      • 📋 (一)过程活动记录
      • 🔗 (二)嵌套过程定义中非局部量的访问
      • 📦 (三)嵌套程序块的非局部量访问
      • 🌌 (四)动态作用域VS静态作用域:变量查找的两种世界观
    • ⏳ 三、过程调度:函数的接力赛裁判
    • 📖 四、PL/0编译程序:极简运行时的教科书案例
      • 📊 (一)PL/0程序运行栈中的过程活动记录
      • 🧮 (二)实现过程调用的类P-code指令
    • 🏷️ 五、面向对象语言:存储分配的进阶玩法
      • 👥 (一)类和对象的角色
      • 🚀 (二)面向对象程序运行时的特征
      • 📦 (三)对象的存储组织
      • 🔄 (四)例程的动态绑定:多态的幕后推手
      • 🌐 (五)其他话题:OOP存储的优化 trick
      • 🌟 章结


🔍 编译原理——运行时存储组织与内存管理

在这里插入图片描述

🌐 一、运行时存储组织概述

🔧 (一)运行时存储组织的作用与任务

运行时存储组织是编译器后端的"内存管家",核心使命是在程序运行时高效管理内存资源。其关键任务包括:

  • 空间规划师:将内存划分为代码区、数据区、栈区、堆区等逻辑区域,各司其职(如代码区存指令、栈区管函数调用);
  • 生命周期管理员:根据变量作用域(全局/局部/动态)分配存储策略,确保数据"生得其所,死得其时";
  • 地址翻译官:将符号地址(如变量名count)映射到物理内存地址,支持程序正确读写数据。

📦 (二)程序运行时存储空间的布局

以C语言程序为例,运行时内存像一个分层收纳盒,典型布局如下:

  1. 代码区(Text Segment):只读的机器指令,多个进程可共享(如多个QQ进程共享同一代码段);
  2. 数据区(Data Segment)
    • 初始化数据段:存int global=10等"有预设值"的全局变量;
    • BSS段:存int uninit_global等未初始化全局变量,自动填0;
  3. 栈区(Stack Segment):LIFO的局部变量仓库,函数调用时自动分配/释放(如void func(){int x;}x);
  4. 堆区(Heap Segment):动态分配的"自由市场",由malloc/new按需申请(如int* ptr = (int*)malloc(4);)。

🎯 (三)存储分配策略

三种分配策略如同三种快递服务,适配不同"货物"(变量):

  • 静态分配(编译时下单)
    ✅ 适用:全局变量、固定大小数组(如int arr[10];),地址编译时敲定;
    ⚡ 优点:访问快(直接按固定地址取货),无运行时开销;
  • 栈式分配(随调随到)
    ✅ 适用:函数局部变量、递归调用,调用时压栈,返回时弹栈;
    🧩 实现:栈帧(Activation Record)管理,自动处理生命周期;
  • 堆式分配(按需定制)
    ✅ 适用:动态数据结构(链表、动态数组),生存期由程序控制;
    ⚠️ 注意:需手动free/delete,否则内存泄漏(如"借了快递箱不还")。

🧩 二、活动记录:函数调用的幕后账本

📋 (一)过程活动记录

函数调用时,内存会创建一本"活动账本"——活动记录(栈帧),典型结构如下:

+-------------------+
| 返回地址(RA)     | 函数干完活该回哪条指令  
+-------------------+
| 动态链(DL)       | 指向调用者账本(用于返回时结账)  
+-------------------+
| 静态链(SL)       | 指向静态外层账本(找非局部变量)  
+-------------------+
| 形式参数(Args)   | 调用者传来的"快递包裹"  
+-------------------+
| 局部变量(Locals) | 函数自己的"办公用品"  
+-------------------+

示例:调用int add(int a, int b){return a+b;}时,栈帧会存ab的值,以及返回地址(调用者下一条指令)。

🔗 (二)嵌套过程定义中非局部量的访问

当函数套娃(如void outer(){ void inner(){...} }),inner找outer的变量时,靠静态链(SL)搭起"记忆桥梁":

  1. inner的活动记录SL指向outer的活动记录;
  2. 访问outer的x时,沿SL链直接去outer账本取数;
  3. 多层嵌套时,像爬楼梯一样逐层往上找,直到找到目标变量。

📦 (三)嵌套程序块的非局部量访问

程序块(如{ int x=5; })的变量访问像"就近原则":

  • 先查当前块内变量(如块内x=5);
  • 若没有,沿作用域链查外层块或函数(如函数内的x=10);
  • 全局变量是最终保底(如全局x=20)。
    示例
int x=100;
void func(){int x=10;{ int x=1; printf("%d", x); } // 输出1(优先块内x)
}

🌌 (四)动态作用域VS静态作用域:变量查找的两种世界观

特性静态作用域(词法作用域)动态作用域(调用链作用域)
决定因素代码文本中的声明位置(写死的)运行时的函数调用顺序(动态变化)
典型语言C/Java/Python(大多数语言)Lisp/Shell(少数动态语言)
查找示例def outer(){x=10; def inner(){print(x);}}
inner()输出10(静态外层x)
(defun outer () (let ((x 'outer)) (inner)))
inner()输出’outer(调用链中的x)
性能快(编译时确定路径)慢(运行时遍历调用链)

⏳ 三、过程调度:函数的接力赛裁判

过程调度如同运动会的接力赛裁判,管理函数的"上场"与"下场":

  • 调用链管理:用栈保存每个函数的活动记录,确保main→A→B的调用顺序正确返回;
  • 上下文切换:保存当前函数的寄存器状态(如累加器值),切换到被调用函数时恢复;
  • 异常处理:当函数发生栈溢出(如无限递归),调度器及时喊停,避免程序崩溃。

实现方式

  • 栈式调度:C语言的函数调用,靠栈帧压入弹出实现;
  • 协程调度:Python的async/await,允许函数主动交出控制权,实现非阻塞编程;
  • 多线程调度:操作系统级的线程调度,用时间片轮转让多个函数"插队"执行。

📖 四、PL/0编译程序:极简运行时的教科书案例

📊 (一)PL/0程序运行栈中的过程活动记录

PL/0是编译原理的"教学版小火车",其运行栈帧设计极其精简:

  • 三链核心
    • 静态链(SL):指向静态外层过程,用于找非局部变量;
    • 动态链(DL):指向调用者,用于返回时恢复现场;
    • 返回地址(RA):记录"返程票"位置;
  • 示例场景:嵌套过程P调用QQ的SL指向P的活动记录,确保Q能访问P的变量。

🧮 (二)实现过程调用的类P-code指令

PL/0虚拟机用简单指令模拟函数调用,关键指令如下:

  1. CALL a:调用函数,参数个数为a,自动创建栈帧;
  2. RET:函数返回,销毁当前栈帧,按RA回到调用者;
  3. LDA n,x:从静态链第n层取变量xn=0为当前层,n=1为外层)。
    执行示例
// 调用函数f(a,b)
CALL 2  ; 分配2个参数空间,创建栈帧
LDA 0,a ; 取当前层变量a
ADD     ; 累加器加b
RET     ; 返回结果

🏷️ 五、面向对象语言:存储分配的进阶玩法

👥 (一)类和对象的角色

在OOP世界,类是"蓝图",对象是"成品":

  • 类(Class):定义属性(如int age)和方法(如void sayHi());
  • 对象(Object):类的实例,像按蓝图造出的房子,有独立的属性值。

🚀 (二)面向对象程序运行时的特征

相比传统语言,OOP运行时有三大魔法:

  1. 封装:对象的属性被藏起来,只能通过方法访问(如person.setAge(18)而非直接改age);
  2. 继承:子类复用父类的属性和方法(如Student extends Person),存储时子类对象包含父类字段;
  3. 多态:同一方法名对应不同实现(如Animal.speak()对狗是"汪汪",对猫是"喵喵"),靠动态绑定实现。

📦 (三)对象的存储组织

对象在内存中的布局像一个带索引的抽屉:

  • 对象头(Object Header):存类指针(指向类元数据)、哈希码等;
  • 实例数据(Instance Data):按声明顺序存属性值(如int age=18);
  • 对齐填充(Padding):内存对齐用的"填充物",提升访问效率。
    示例(Java对象)
class Person {String name; // 引用类型,存指针int age;     // 基本类型,直接存值
}
// 内存中:对象头 + name指针 + age整数 + 对齐填充

🔄 (四)例程的动态绑定:多态的幕后推手

动态绑定让OOP语言实现"晚期决定":

  1. 虚函数表(VTable):每个类有一张表,存方法的实际地址;
  2. 调用流程
    • 对象p调用p.speak()时,先查p的类指针;
    • 找到类的VTable,根据方法名取对应地址;
    • 跳转执行(如Dog.speak()Cat.speak())。
      优势:编译时无需确定具体实现,运行时灵活切换,如插件系统可动态加载不同实现类。

🌐 (五)其他话题:OOP存储的优化 trick

  • 对象池(Object Pool):重复利用已创建的对象(如数据库连接池),减少堆分配开销;
  • 逃逸分析(Escape Analysis):Java HotSpot会分析对象是否会"逃出"当前方法,若不会则栈上分配(如局部对象new Person());
  • 分代垃圾回收(Generational GC):根据对象存活时间分区回收(新生代、老年代),提升GC效率。

🌟 章结

从基础的栈堆分配到OOP的动态绑定,运行时存储组织如同编译原理的"内存魔术"。理解这些机制,不仅能看透程序的内存行为(如Java的GC调优),更能为开发编译器、虚拟机打下基础——毕竟,每一行代码的背后,都是一场精密的内存芭蕾。

http://www.dtcms.com/a/263523.html

相关文章:

  • Zookeeper安装使用教程
  • 回写缓存为何需要脏位?
  • SimLOD代码精读(二)建立Octree之Splitting Pass分裂阶段
  • 英国研究团队启动合成完整人类基因组的前沿项目
  • Java-包-访问修饰符-封装
  • Redis Lua 调试器(LDB)完全指南
  • 深度剖析 LNK 参数隐藏攻击 (ZDI-CAN-25373)
  • C++ Vector的使用(下)
  • 贪心算法在C++中的应用与实践
  • 基于动漫数据的可视化分析与推荐系统实现
  • Pyhton-EXCEL与Mysql数据对比
  • Monorepo+Pnpm+Turborepo
  • Vue Vue-route (1)
  • jvm的调优命令jstack打印堆栈信息阐述以及调优
  • Linux信号量
  • 基础算法合集-图论
  • 《AI的“三体进化”:数字基因与超人类思维的奇点降临》
  • Windows 11 24H2更新系统后WiFi不显示故障处理
  • AI编程实战:Cursor黑科技全解析
  • Python 数据分析与机器学习入门 (二):NumPy 核心教程,玩转多维数组
  • 【C语言】知识总结·内存函数
  • CSDN博客大搬家(本地下载markdown合适和图片本地化)
  • I/O I/O基本概念与基本I/O函数 6.30
  • Swift 实现二叉树垂直遍历:LeetCode 314 完整解析与实战示例
  • HTML之常用基础标签
  • Stable Diffusion 项目实战落地:从0到1 掌握ControlNet 第四篇 风格化字体大揭秘:从线稿到涂鸦,ControlNet让文字焕发新生
  • C#索引和范围:简化集合访问的现代特性详解
  • 湖北理元理律师事务所债务解法:从法律技术到生活重建
  • 使用nomachine远程连接ARM设备桌面
  • 【SpringAI】3.结构化输出,初级版