运行时与内存管理 (★★★)
#memory-mgmt
程序加载过程
当 ABAP 程序首次被调用时,它经过多阶段加载管道:
1 2 3 4 5
| 源代码 生成 运行时对象 PXA 缓冲区 (仓库) → (生成的代码) → (可执行形式) → (共享内存) .abap 加载版本 加载的实例 仅加载一次 (DB 表) (DB 表) (每个内部 (应用服务器 会话) 级别)
|
| 阶段 |
位置 |
范围 |
说明 |
| 源代码 |
数据库(仓库) |
系统范围 |
ABAP 源文本 |
| 生成 |
数据库(加载版本) |
系统范围 |
生成/编译的代码 |
| 运行时对象 |
PXA 缓冲区(程序执行区) |
每个应用服务器 |
缓存的可执行代码 |
| 运行时实例 |
滚动区 |
每个内部会话 |
当前执行的运行时数据 |
PXA 缓冲区在应用服务器级别保存运行时对象。如果用户 A 已经调用了程序 ZPROG,用户 B 调用同一程序时可以复用 PXA 条目 – 无需重新生成。
会话架构
1 2 3 4 5 6 7 8 9 10 11
| 用户会话(终端会话) ├── 外部会话 1(GUI 窗口) │ ├── 内部会话(栈) │ │ ├── 主程序 + 被调用程序 │ │ ├── 附加程序组 1 │ │ └── 附加程序组 2 │ └── ABAP 内存(EXPORT/IMPORT) ├── 外部会话 2(另一个 GUI 窗口) │ ├── 内部会话(栈) │ └── ABAP 内存(与会话 1 分开) └── SAP 内存(SET/GET PARAMETER -- 所有外部会话共享)
|
| 会话类型 |
范围 |
生命周期 |
| 用户会话 |
一次 SAP 登录 |
直到注销 |
| 外部会话 |
一个 GUI 窗口/模式 |
直到窗口关闭 |
| 内部会话 |
栈上的一个程序调用 |
直到程序结束或栈展开 |
程序组
每个内部会话包含至少一个程序组。加载到同一组中的程序共享全局数据。
| 程序组类型 |
创建者 |
数据共享 |
| 主程序组 |
内部会话中的第一个程序 |
此组中所有程序共享全局数据 |
| 附加程序组 |
通过过程调用加载的函数组、类池 |
每个组有独立的全局数据 |
程序如何进入程序组
| 调用类型 |
程序组 |
风险 |
外部子程序调用(PERFORM form(prog)) |
同一(主)程序组 |
接口工作区的意外数据共享 |
函数模块调用(CALL FUNCTION) |
独立程序组(函数组加载到自己的组中) |
安全 – 隔离的全局数据 |
| 类方法调用 |
独立程序组(类池) |
安全 – 隔离 |
外部 PERFORM 将目标程序加载到同一程序组,共享接口工作区(如经典选择屏幕或 GET/SET PARAMETER 内存区域)。这可能导致意外的副作用。建议使用函数模块或类方法。
程序调用的内存管理
| 语句 |
会话效果 |
程序组 |
SUBMIT ... AND RETURN |
新内部会话压入栈 |
与调用者分离 |
CALL TRANSACTION |
新内部会话压入栈 |
与调用者分离 |
SUBMIT(无 AND RETURN) |
顶层内部会话被替换 |
新的主程序组 |
LEAVE TO TRANSACTION |
整个栈被销毁,创建新会话 |
全新开始 |
会话栈行为
1 2 3 4 5 6 7 8 9
| 初始状态: SUBMIT AND RETURN 后: LEAVE TO TRANSACTION 后: (栈向上增长) ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ 会话 A │ │ 会话 B │ │ 会话 X │ │ (程序 P1) │ │ (提交的 P2) │ │ (事务) │ └───────────────┘ │───────────────│ └───────────────┘ │ 会话 A │ │ (程序 P1) │ 所有先前的会话被销毁 └───────────────┘ ABAP 内存也被清除
|
函数组和类:会话行为
| 方面 |
函数组 |
类(ABAP Objects) |
| 加载到 |
每个函数组独立的程序组 |
每个类独立的程序组 |
| 全局数据 |
每个内部会话实例 |
每个内部会话实例 |
| 跨会话共享? |
否 – 每个会话有自己的副本 |
否 – 每个会话有自己的副本 |
| 生命周期 |
会话中首次调用 FM 时加载,持续到会话结束 |
首次创建对象时加载,持续到会话结束 |
如果两个内部会话调用同一函数组的 FM,每个会话获得函数组全局数据的独立副本。内部会话之间没有共享。
ABAP 内存 vs SAP 内存
| 方面 |
ABAP 内存 |
SAP 内存 |
| 语句 |
EXPORT ... TO MEMORY ID、IMPORT ... FROM MEMORY ID、FREE MEMORY ID |
SET PARAMETER ID、GET PARAMETER ID |
| 范围 |
每个外部会话 |
每个用户会话 |
| 内部会话间共享? |
是 – 同一外部会话中的所有内部会话共享 |
是 – 所有外部会话共享 |
| 外部会话间共享? |
否 |
是 |
| 支持的数据类型 |
任意(平坦、深层、对象) |
仅平坦基本字段(通过 SPA/GPA 参数) |
| 生命周期 |
直到外部会话关闭或 FREE MEMORY |
直到用户注销 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| " ABAP 内存 -- 写入 EXPORT lv_data = 'Hello' TO MEMORY ID 'MY_DATA'.
" ABAP 内存 -- 读取 IMPORT lv_data = lv_result FROM MEMORY ID 'MY_DATA'.
" ABAP 内存 -- 清除 FREE MEMORY ID 'MY_DATA'.
" SAP 内存 -- 写入 SET PARAMETER ID 'BUK' FIELD '1000'.
" SAP 内存 -- 读取 GET PARAMETER ID 'BUK' FIELD lv_bukrs.
|
内存范围图示
1 2 3 4 5 6 7 8 9 10 11 12 13
| 用户会话 ┌─────────────────────────────────────────────────────────────┐ │ SAP 内存(SET/GET PARAMETER -- 所有窗口共享) │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ 外部会话 1 │ │ 外部会话 2 │ │ │ │ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │ │ │ │ ABAP 内存 1 │ │ │ │ ABAP 内存 2 │ │ │ │ │ │ (EXPORT/IMPORT) │ │ │ │ (EXPORT/IMPORT) │ │ │ │ │ │ 此外部会话中的 │ │ │ │ 此外部会话中的 │ │ │ │ │ │ 内部会话共享 │ │ │ │ 内部会话共享 │ │ │ │ │ └──────────────────────┘ │ │ └──────────────────────┘ │ │ │ └──────────────────────────┘ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
|
动态数据对象内存
深层数据对象(字符串、内表)使用基于引用的内存模型,包含三个部分:
| 组件 |
描述 |
| 引用 |
保存在声明变量中(指针) |
| 头部 |
管理元数据(长度、容量、类型信息) |
| 数据体 |
实际内容(字符、行) |
1 2 3 4 5
| 变量(引用) 头部 数据体 ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ ref to data │──>│ length, cap, │───>│ actual content │ │ │ │ type info │ │ (chars / rows) │ └──────────────┘ └──────────────┘ └──────────────────┘
|
写入时复制语义(共享)
当字符串或内表赋值给另一个变量时,ABAP 使用写入时复制(共享)来节省内存:
| 阶段 |
发生了什么 |
内存 |
初始赋值 t2 = t1 |
仅复制引用;两者指向同一头部和数据 |
最小 – 仅一个指针 |
| 通过任一变量读取 |
两者读取同一共享数据体 |
无复制 |
| 通过一个变量写入/修改 |
数据体为修改的变量复制;另一个保持原始数据 |
仅在修改时复制 |
1 2 3 4 5 6
| DATA(lv_text1) = |ABCDE|. DATA(lv_text2) = lv_text1. " 共享:两者指向同一数据
lv_text2 = |XYZ|. " 写入时复制:lv_text2 现在有自己的副本 " lv_text1 仍 = 'ABCDE' " lv_text2 = 'XYZ'
|
对于传递给子程序或赋值给局部变量的大型内表,ABAP 最初仅复制引用。只有当数据实际被修改时才会进行昂贵的数据复制。
封装组件(Boxed Components)
封装组件使用 BOXED 附加项用于结构中的深层组件,启用初始值共享(IVS)。
语法
1 2 3 4
| TYPES: BEGIN OF ty_structure, field1 TYPE i, sub TYPE ty_inner_struct BOXED, " 封装组件 END OF ty_structure.
|
封装组件的工作原理
| 方面 |
封装组件 |
普通深层组件 |
| 初始值内存 |
共享 – 所有初始实例指向一个共享头部/数据 |
每个实例分配自己的头部(即使为空) |
| 非初始值内存 |
首次写入时分配自己的头部+数据体 |
已有自己的分配 |
| 使用场景 |
有大量实例且大多保持初始值的大型结构 |
大多数实例都会有数据时 |
初始值共享图示
1 2 3 4 5 6 7 8 9 10 11 12
| 不使用 BOXED:每个结构分配自己的(空)头部 ┌─────────┐ ┌─────────┐ ┌─────────┐ │ sub ──────>│ (empty) │ │ (empty) │ │ (empty) │ (3 个头部) └─────────┘ └─────────┘ └─────────┘ └─────────┘
使用 BOXED:所有初始实例共享一个头部 ┌─────────┐ ┌─────────┐ ┌─────────┐ │ sub ──────>│ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ (1 个共享头部) └────┬────┘ └────┬────┘ └────┬────┘ └───────────┴───────────┘ 共享初始值
|
何时使用封装组件
| 适用场景 |
原因 |
| 子结构通常为空的内表(多行) |
每个空行节省一个头部分配 |
| 大型数据集中的嵌套结构 |
累计节省跨多个实例 |
| 默认/初始状态很常见 |
IVS 避免冗余的空分配 |
| 不适用场景 |
原因 |
| 大多数实例都会有数据 |
没有共享收益;增加开销 |
| 实例数量少 |
内存节省微乎其微 |
使用 BOXED 附加项的封装组件启用初始值共享 – 所有未修改的实例共享单个内存分配用于初始(空)状态。仅在组件首次被写入时才单独分配内存。
考试/测试模式
| 关键词 / 模式 |
正确答案 |
| PXA 缓冲区 |
应用服务器级别;保存运行时对象,所有用户共享 |
| 滚动区 |
每个内部会话;保存局部数据和程序实例 |
| SUBMIT AND RETURN vs SUBMIT |
AND RETURN = 插入(新会话入栈);不带 = 替换(栈顶会话被替换) |
| LEAVE TO TRANSACTION |
销毁整个栈;ABAP 内存被清除 |
| 外部子程序调用(PERFORM) |
加载到同一程序组 – 有意外数据共享的风险 |
| 函数模块/方法调用 |
加载到独立程序组 – 隔离的数据 |
| ABAP 内存范围 |
每个外部会话;其中的内部会话共享 |
| SAP 内存范围 |
每个用户会话;所有外部会话共享 |
| EXPORT/IMPORT/FREE MEMORY ID |
ABAP 内存操作 |
| SET/GET PARAMETER ID |
SAP 内存操作(仅平坦字段) |
| 写入时复制(共享) |
赋值时复制引用;仅在修改时复制数据 |
| BOXED 附加项 |
为深层结构组件启用初始值共享 |
| 同一函数组,不同会话 |
每个会话获得全局数据的独立副本 – 不共享 |
| 主程序组 |
由内部会话中的第一个程序创建 |
相关笔记
- 程序调用 – SUBMIT、CALL TRANSACTION、LEAVE TO TRANSACTION 详情
- 共享对象 – 通过共享内存实现跨会话和跨用户数据共享
- [[_posts/SAP_MM/SCM500_采购流程/00-仪表盘/考试陷阱]] – 常见内存管理陷阱
- [[_posts/SAP_MM/SCM500_采购流程/00-仪表盘/快速参考]] – 内存区域汇总表