运行时与内存管理

运行时与内存管理 (★★★)

#memory-mgmt

程序加载过程

当 ABAP 程序首次被调用时,它经过多阶段加载管道:

1
2
3
4
5
源代码              生成                  运行时对象              PXA 缓冲区
(仓库) → (生成的代码) → (可执行形式) → (共享内存)
.abap 加载版本 加载的实例 仅加载一次
(DB 表) (DB 表) (每个内部 (应用服务器
会话) 级别)
阶段 位置 范围 说明
源代码 数据库(仓库) 系统范围 ABAP 源文本
生成 数据库(加载版本) 系统范围 生成/编译的代码
运行时对象 PXA 缓冲区(程序执行区) 每个应用服务器 缓存的可执行代码
运行时实例 滚动区 每个内部会话 当前执行的运行时数据
💡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 IDIMPORT ... FROM MEMORY IDFREE MEMORY ID SET PARAMETER IDGET 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-仪表盘/快速参考]] – 内存区域汇总表