こんにちは。基幹システムの現場で日々、COBOLの山やPL/Iの複雑なポインタ制御と格闘しているエンジニアの皆さん、お疲れ様です。
メインフレームの寿命は長い。私たちが若手だった頃に構築された勘定系や物流系のバッチ基盤が、今も何百万件というレコードを夜通し処理し続けています。その中で、ふとソースコードを開いたときに「なんじゃこりゃ」と頭を抱えたくなる瞬間はありませんか?そう、CONTROLLED属性とALLOCATE/FREEのコンボ技、そしてそれに伴うメモリリークの残骸です。
COBOLにはない、このPL/I特有のダイナミック・ストレージ制御。今回は、スタック管理の常識を覆すこのヒープ領域の魔力と、夜間バッチを突然の「S80A(GETMAIN/FREEMAINエラー)」で落とさないための実践的な防衛策を、現場のノウハウを交えて徹底的に解説します。
—
1. なぜ今、CONTROLLED属性なのか?(スタックとヒープの根本的な違い)
私たちが普段何気なく定義する変数(AUTOMATIC属性、省略時のデフォルト)は、ブロック(PROCEDUREやBEGIN-END)の開始時にスタック領域に割り当てられ、ブロックを抜ければ自動的に消滅します。これは非常に安全ですが、「処理するデータ量が実行時まで分からない」「レコード件数に応じて動的にメモリサイズを変えたい」という要件の前無力です。
そこで登場するのが CONTROLLED属性 です。
1
DCL WK_REC CHAR(100) CONTROLLED;
この宣言をした瞬間、コンパイラはこの変数に対して「まだメモリを割り当てていないよ」というフラグを立てます。メモリが実際に割り当てられるのは、私たちが ALLOCATE文 を実行した瞬間です。そして、使い終わったら FREE文 で明示的に解放する。これはC言語の `malloc` / `free` や、Javaなどのガベージコレクションとは異なり、プログラマが完全にライフサイクルを支配する世界です。
COBOL脳からの脱却:識別子の自由さと「予約語」の罠
PL/Iの面白い(そして恐ろしい)ところは、COBOLのような厳格な予約語の縛りがほとんどない点です。例えば、`ALLOCATE` や `FREE` すら、文脈によっては変数名として使えてしまいます(もちろん、そんな狂ったコーディングをする人はいませんが)。
しかし、この「言語仕様の懐の深さ」が、動的メモリ管理におけるヒューマンエラーの温床になります。予約語に守られていないからこそ、規律正しいコーディング標準が必要なのです。
—
2. VSAMアクセスと動的メモリ管理の現場的ジレンマ
夜間バッチでよくあるユースケースを考えてみましょう。
KSDS(Key-Sequenced Data Set)のVSAMファイルを読み込み、可変長の関連レコード群をメモリ上に保持しながらツリー構造を構築して処理する――こんな時、レコードの最大数が見積もれない、あるいはメモリ節約のために「必要な分だけ」動的に確保したい場合に `CONTROLLED` が真価を発揮します。
しかし、ここに大きな罠があります。「ALLOCATEした回数分、必ずFREEしなければならない」という鉄則です。もしループ内でアロケートし続け、エラー発生時にフリー漏れを起こすと、瞬く間にリージョン域を食いつぶし、夜間バッチは無残にもアベンドします。
これを安全に制御するためには、後述する ONユニット(例外処理) との組み合わせが不可欠です。
—
3. 実践!CONTROLLED属性を活用した安全なバッチ処理プログラム
百聞は一見に如かず。実際にVSAM(または順編成ファイル)の可変長レコードを動的領域で受け受け、安全に処理・解放する実用的なPL/Iソースコードを見てください。大文字ベースで記述し、現場ですぐに使えるコメントを付与しています。
1
/ ————————————————- /
/ プログラム名: DYNMEM01 /
/ 概要: CONTROLLED属性による動的メモリ管理と /
/ VSAMレコード処理のサンプル /
/ ————————————————- /
DYNMEM01: PROC OPTIONS(MAIN);
/ 制御用変数およびファイル定義 /
DCL IN_FILE FILE RECORD INPUT;
DCL IO_EOF BIT(1) INIT(‘0’B);
/ CONTROLLED属性を持つ動的構造体(レコードバッファ) /
DCL 1 NODE_REC CONTROLLED,
2 N_KEY CHAR(8),
2 N_DATA_LEN FIXED BIN(15),
2 N_PAYLOAD CHAR(0) CONTROLLED; / さらに可変長の部分 /
/ 例外条件(ONユニット)の宣言 /
ON ENDFILE(IN_FILE) IO_EOF = ‘1’B;
/ 異常終了時のメモリ解放漏れを防ぐための安全弁 /
ON ERROR BEGIN;
DISPLAY(‘ 予期せぬエラーが発生しました。クリーンアップを実行します ‘);
CALL EMERGENCY_FREE;
SIGNAL ERROR; / 上位の異常終了ハンドラへ委譲 /
END;
OPEN FILE(IN_FILE);
/ メインの読み込み・処理ループ /
DO WHILE(^IO_EOF);
READ FILE(IN_FILE) INTO(WK_DUMMY_AREA); / 実際にはレコード長に応じた処理 /
/ — ここから動的メモリ確保 — /
/ 例として、データ長が実行時に500バイトと判明したとする /
ALLOCATE NODE_REC;
ALLOCATE N_PAYLOAD CHAR(500) IN(NODE_REC); / 領域内にサブアロケート /
N_KEY = ‘KEY00001’;
N_DATA_LEN = 500;
N_PAYLOAD = ‘テストデータ内容…’;
/ 処理サブルーチンの呼び出し /
CALL PROCESS_NODE(NODE_REC);
/ — 忘れずにメモリ解放(FREE) — /
/ 注意: サブアロケートされたN_PAYLOADも親のFREEで連動解放される /
FREE NODE_REC;
END;
CLOSE FILE(IN_FILE);
RETURN;
/ ——————————————— /
/ ノード処理サブルーチン /
/ ——————————————— /
PROCESS_NODE: PROC(P_NODE);
DCL 1 P_NODE CONTROLLED,
2 N_KEY CHAR(8),
2 N_DATA_LEN FIXED BIN(15),
2 N_PAYLOAD CHAR(0) CONTROLLED;
/ ビジネスロジックをここに記述 /
DISPLAY(‘処理中キー: ‘ || N_KEY);
END PROCESS_NODE;
/ ——————————————— /
/ 緊急解放用ルーチン(エラー時のメモリリーク防止) /
/ ——————————————— /
EMERGENCY_FREE: PROC;
/ ALLOCATED(変数名) ビルトイン関数で割り当て状態を確認してから解放 /
IF ALLOCATED(NODE_REC) THEN
FREE NODE_REC;
END EMERGENCY_FREE;
END DYNMEM01;
—
4. 現場のプロが教えるデバッグとメモリリーク対策の極意
上記のコードでも触れましたが、PL/Iで動的メモリを扱う上で、シニアエンジニアとして後輩に必ず伝えている鉄則が3つあります。
① `ALLOCATED` ビルトイン関数を過信せず、しかし必ず使え
変数が現在メモリ上に存在するかどうかを判定するには、`ALLOCATED(変数名)` という極めて便利なビルトイン関数があります。
「二重ALLOCATE」によるメモリの上書きエラーや、「割り当てられていない変数のFREE」によるS0C4(アドレス例外)を防ぐため、条件分岐には必ずこれを挟むのが防衛的プログラミングの基本です。
1
IF ^ALLOCATED(WK_BUFFER) THEN
ALLOCATE WK_BUFFER;
② ON-UNIT(異常系制御フロー)との連動は必須事項
バッチプログラムが途中で異常終了( Abend )したとき、OSはアドレススペースのメモリを回収してくれますが、それはあくまで「プロセス終了時」の話です。
しかし、大規模バッチの中で、同一アドレススペース内でのサブタスクや、動的リンクされたモジュール群、あるいはCICSオンラインのタスク内(※CICS環境でのCONTROLLED利用は御法度ですが例えとして)でメモリリークを起こすと、システム全体のパフォーマンス低下を招きます。
先ほどのサンプルコードのように、`ON ERROR` ブロックを定義し、異常時でも確実に `FREE` が通る仕組み(クリーンアップハンドラ)を設計段階で組み込んでおくべきです。
③ 世代管理やポインタの迷子に気をつけろ
CONTROLLED変数は、スタックのように「直近のものを自動で隠す(世代管理)」機能も持っています。同じ変数名で何度も `ALLOCATE` を実行すると、古いメモリ領域が隠され(PUSH)、新しい領域が表に出ます。そして `FREE` を呼ぶと、今度は隠されていた古い領域が復活(POP)します。
……お分かりでしょうか? この仕様を正しく理解していないと、「何度FREEしてもメモリリークが消えない(実は奥に古い世代が眠っている)」という、悪夢のようなデバッグ地獄に引きずり込まれます。
実務のコーディング標準としては、「1つのCONTROLLED変数に対して、世代を重ねるようなトリッキーな使い方は禁止。必ず 1ALLOCATE 対 1FREE のペアを担保する」と定めておくのが無難です。
—
おわりに
PL/Iの `CONTROLLED` 属性と `ALLOCATE/FREE` は、正しく扱えばメインフレームのリソースを極限まで効率化できる強力な武器です。しかし、一歩間違えれば夜間バッチを沈没させる爆弾にもなります。
「動的メモリ管理だから難しそう」と避けるのではなく、言語仕様の裏側にあるメモリのライフサイクルを頭に焼き付け、確実な例外処理(ONユニット)とビルトイン関数(`ALLOCATED`)によるガードを固めること。それこそが、時代遅れと言われようとも基幹システムを支え続ける私たちメインフレーム・エンジニアの矜持です。
次回のバッチ改修では、ぜひ自信を持ってこの動的制御を設計・レビューしてください。あなたの書いたコードが、次の10年も止まらないシステムを支える基盤になります。
