【実務・中級編】ALLOCATE文による動的ストレージ獲得とヒープ管理 – PL/Iの基本構文とデータ制御実践ガイド

おい、最近夜間バッチのウィンドウがじわじわ圧迫されて、運用チームから「またストレージ不足でabend(異常終了)したぞ」って泣きが入ってないか?

メインフレームの現場で長く生きていると、こういう理不尽なリソース枯渇のトラブルに何度も直面するものだ。COBOLからPL/Iへ移行したプロジェクトや、歴史ある巨大な金融・流通系の基幹システムを保守していると、大抵の犯人は「ヒープ領域の肥大化と、FREE文の解放漏れ」、つまりストレージリークだ。

PL/Iは非常に強力な言語で、C言語のようにポインタ(POINTER)を直接操り、`ALLOCATE`文で動的にメモリを掴みに行くことができる。しかし、その自由度の高さゆえに、適切なライフサイクル管理を怠ると、メインフレームの貴重な仮想記憶(24ビットアドレッシングの領域や、上位の31ビット/64ビットハイアドレス領域)を食いつぶすことになる。

今回は、PL/Iの動的ストレージ獲得のメカニズムと、実務で絶対に知っておくべきヒープ管理、そしてストレージリークを炙り出すための実践的なデバッグ手法を、現場のノウハウを交えて徹底的に解説しよう。

1. PL/I動/的ストレージ管理の基本と「予約語を持たない」懐の深さ

まず大前提として、PL/Iの言語仕様における大きな特徴に触れておこう。
PL/Iには、いわゆる「予約語(Reserved Words)」という概念が厳密には存在しない。例えば、`IF`や`THEN`、`ALLOCATE`といったキーワードであっても、文脈上区別がつくため、プログラマが変数名や識別子として定義してしまおうと思えばできてしまう(※もちろん、そんな混乱を招くコーディングはコードレビューで即座にリジェクトだがな)。

この柔軟性ゆえに、変数名や構造体要素名に言語キーワードが紛れ込みやすい。特に動的ストレージを扱う際は、ポインタ変数やベース付き変数(BASED変数)の命名規則をチーム内で厳格に統一しておくことが、保守性向上の第一歩だ。

ALLOCATE文とBASED変数のメカニズム

PL/Iで動的メモリをヒープ(Heap)から取得するには、`ALLOCATE`(省略形 `ALLOC`)を使用する。

DCL 1 MY_RECORD BASED(P_REC),
2 REC_ID CHAR(8),
2 REC_DATA CHAR(100);

DCL P_REC POINTER;

/ ヒープ領域からメモリを獲得 /
ALLOCATE MY_RECORD;

この時何が起きているか?
`ALLOCATE MY_RECORD`を実行すると、コンパイラは動的ストレージ(ヒープ)から `MY_RECORD` の構造体の大きさに応じた領域を割り当て、そのメモリアドレスをポインタ変数 `P_REC` に自動的に格納する。

ここで注意すべきは、「誰がこのメモリの寿命を管理しているか」だ。
自動変数(AUTOMATIC)であれば、そのブロック(PROCEDUREやBEGINブロック)を抜けた瞬間に自動的に解放される。しかし、ポインタ経由で `ALLOCATE` したBASED変数は、プログラマが明示的に `FREE` 文を発行するまで、ヒープに居座り続ける。これがリークの温床になる。

2. VSAMレコード処理とONユニットが絡む「最悪のシナリオ」

実際のバッチ処理で、どのようなときにストレージリークが起きるのか。
よくあるのが、大量のVSAM(KSDSなど)レコードを読み込み、関連する子テーブルの情報を動的にメモリ上に展開しながら処理していくようなマスター更新・突合バッチだ。

ここに、PL/I特有の強力なエラー処理機構であるONユニット(ON-unit)と例外処理が絡むと、解放漏れ事故が一気に跳ね上がる。

以下の実用的なサンプルコードを見てほしい。大文字ベースで、現場のコーディング標準に合わせた書き方をしている。

DCL WORK_PROC PROC OPTIONS(MAIN);

/ VSAMファイルの定義(実際はFIPSやマクロ等で記述) /
DCL MASTER_FILE FILE RECORD SEQUENTIAL INPUT;

/ ベース付きレコード構造体の定義 /
DCL 1 DETAIL_NODE BASED(P_NODE),
2 NEXT_PTR POINTER, — 次のノードへのポインタ —
2 KEY_VAL CHAR(16), — キー情報 —
2 PAYLOAD CHAR(1024); — 実データ —

DCL P_HEAD POINTER INIT(NULL());
DCL P_NODE POINTER;
DCL P_TEMP POINTER;
DCL EOF_FLAG BIT(1) INIT(‘0’B);

— 異常終了時のハンドリングを行うONユニット —
ON ENDFILE(MASTER_FILE) EOF_FLAG = ‘1’B;

ON ERROR
BEGIN;
PUT SKIP LIST(‘ 致命的エラー発生: 処理を中断します ‘);
— 現場のテクニック:エラー時にも確保済みヒープの強制解放を試みるか、
あるいはダンプ採取へ持ち込むかの判断を入れる —
GOTO EMERGENCY_EXIT;
END;

OPEN FILE(MASTER_FILE);

— メインループ:VSAMからの読み込みと動的リスト構造の構築 —
DO WHILE(^EOF_FLAG);
READ FILE(MASTER_FILE) INTO(DUMMY_AREA); — 簡易表現 —

— 動的にメモリを獲得 —
ALLOCATE DETAIL_NODE;

— 万が一のストレージ不足(STG_CONDITION等)を考慮したロジックが必要 —
IF P_NODE = NULL() THEN
BEGIN;
PUT SKIP LIST(‘ERROR: ヒープ領域が枯渇しました’);
SIGNAL ERROR;
END;

— データの詰め替え —
P_NODE->KEY_VAL = ‘SAMPLE_KEY’;
P_NODE->PAYLOAD = ‘TRANSACTION_DATA…’;
P_NODE->NEXT_PTR = P_HEAD; — リストの先頭に追加(LIFO) —
P_HEAD = P_NODE;

END;

CLOSE FILE(MASTER_FILE);

— 通常終了時の解放処理(これが抜けるとストレージリーク!) —
P_NODE = P_HEAD;
DO WHILE(P_NODE ^= NULL());
P_TEMP = P_NODE->NEXT_PTR;

— 領域の解放 —
FREE P_NODE->DETAIL_NODE; — または単に FREE P_NODE; とも記述可能だが構造体名指定が安全 —

P_NODE = P_TEMP;
END;

RETURN;

EMERGENCY_EXIT:
— [解放漏れ防止の防衛策] エラー脱出時も可能な限りヒープを掃除する —
P_NODE = P_HEAD;
DO WHILE(P_NODE ^= NULL());
P_TEMP = P_NODE->NEXT_PTR;
FREE P_NODE->DETAIL_NODE;
P_NODE = P_TEMP;
END;

STOP;

END WORK_PROC;

このコードの後半を見てほしい。
ループを抜けたあと、`P_HEAD` からたどって一つずつ `FREE` を実行している。もし、この解放ループを書き忘れたり、途中で条件分岐(`LEAVE`や`GOTO`)によってスキップしてしまったりすると、その瞬間に対象のメモリブロックはゾンビのようにヒープに残り続け、バッチのイテレーションが回るたびに仮想記憶を食いつぶしていく。

3. ヒープ管理の罠とコンパイラ・オプション(LE/370の挙動)

近代のIBMメインフレーム(z/OS)では、Language Environment(LE/370)という共通ランタイム環境がメモリ管理を裏で仕切っている。
PL/Iの `ALLOCATE` は、内部的には LE のヒープ管理サービスを呼び出してメモリを切り出しているのだ。

ここで、保守エンジニアとして知っておくべき実務の勘所をいくつか伝授しよう。

1. 区域外解放(Wild Free)の恐怖

存在しないアドレスや、すでに `FREE` 済みのポインタに対して再度 `FREE` を実行した場合(二重解放)、LEランタイムはそれを検知して `CEE037Sシステムabend`(S0C4やS806、あるいはCEE要約ダンプを伴うU4038など)を引き起こす。
デバッグ時には、どのポインタがどのタイミングで無効化されたのかを追うために、`CEEOPTS` やランタイムオプションでストレージの初期化(`STORAGE`オプション)を行うことが鉄則だ。

  • 推奨ランタイムオプション例:

`STORAGE(OO,FF,EE)` のように指定して、未初期化の変数や解放済み(Freed)の領域に特定の値(例: `EE`)を書き込ませることで、不正参照や二重解放を早期にバグとして顕在化させる。

2. リークの特定手法(LEフットプリントと診断ツール)

本番稼働中のバッチで「徐々にメモリ使用量が増える」というスローリークに直面した場合、ソースコードを目視で追うのは限界がある。
そんなときは、IBM Debug Toolや、LEのレポート機能(`RPTSTG(ON)`)を活用する。

バッチのJCL実行時、パラメタに以下を仕込むのだ。

//STEP1 EXEC PGM=MYPROG,PARM=’RPTSTG(ON)’
//CEEOPTS DD
HEAPPOOLS(ON)
/

これにより、ステップ終了時にシスログやSYSOUTへ、ヒープストレージの使用状況(何バイト獲得され、何バイト未解放のまま残ったか)のサマリーが出力される。これで「あ、このプログラムは確実に数メガバイト単位でリークしているな」と数字で証明できるようになる。

4. シニアアーキテクトからの実践的提言

最後に、これからPL/Iプログラムの改修や大規模マイグレーションを行うエンジニアへ、確実なコーディング標準を授けよう。

1. 「獲得と解放」は同一のスコープ(あるいは同一のサブルーチン内)で完結させろ
データをあっちのサブルーチンで `ALLOCATE` し、こっちのサブルーチンで `FREE` するようなスパゲッティな設計は絶対にするな。オブジェクト指向のコンストラクタ・デストラクタの概念を意識し、リスト構造を作るにしても、構築と破棄をカプセル化するサブルーチンを必ずペアで用意すること。
2. ON ERRORやON CONDITIONを過信せず、クリーンアップルーチンを通すパスを必ず設計せよ
異常系(abendや例外発生時)でもヒープが放置されないよう、終了処理ルーチン(Clean-up Handler)へ集約するフローチャートを引くこと。
3. バッチの処理件数(コミット間隔)を見直せ
10万件、100万件を一気にメモリ上で連結リストにしようと発想すること自体が、メインフレームのバッチ設計としてはレガシーで悪手だ。適度な件数(例えば数千件ごと)で処理を区切り、こまめに `FREE` を発行してヒープを綺麗にフラッシュしながら流す「チャンク処理(Chunking)」の思想を取り入れろ。

PL/Iは、ハードウェアの性能を極限まで引き出せる素晴らしい言語だ。しかし、メモリ管理の規律を破った者には、容赦なくストレージ枯渇の鉄槌を下す。
今回の解説を胸に刻み、美しく、そして堅牢なメインフレームシステムを維持し続けてくれ。期待しているぞ。

タイトルとURLをコピーしました