Follow

How the Heirloom PL/I Runtime Manages Memory

Table of Contents

  1. Why this matters
  2. The core idea: a structure is a view, not the bytes
  3. Pointers and the 4-byte token
  4. Storage classes = how long memory lives
  5. BASED — overlaying a structure on existing memory
  6. DEFINED — a fixed overlay at an offset
  7. What you'll observe (and how to diagnose it)
  8. What each field looks like in the bytes
  9. Summary
  10. Related articles

Structures, pointers, Global Work Areas (GWA), and the BASED / DEFINED relationship.

Audience: developers and support engineers working with PL/I applications migrated to Java with Heirloom.


Why this matters

On the mainframe, PL/I memory is just addresses into a contiguous region of storage: a structure is a layout, a pointer is an address, and BASED / DEFINED variables are simply windows onto bytes that live somewhere else.

The Heirloom PL/I Runtime preserves these exact semantics on the JVM — but the JVM has no raw addresses, so the runtime models them faithfully with a small, consistent set of building blocks. Understanding that model explains the behavior you see, and turns symptoms like "a field is blank" or "memory keeps growing" into a clear root cause.


1. The core idea: a structure is a view, not the bytes

There are two distinct things in the runtime:

  • The template / view — the PL/I structure (DCL 1 REC ...). It carries the layout (which field is at which offset, and how wide) and a current offset, but it does not own bytes. The same template object is reused, re-pointed at different storage over its life.
  • The memory — a backing byte buffer. This is where the actual data lives.

Think of the structure as a stencil and the buffer as the paper. The stencil tells you where each field sits; the paper holds the ink. Laying the same stencil over different sheets of paper reads/writes different data through the identical field layout.

When you read or write a field, the runtime computes buffer + field-offset and reads/writes the bytes there. This is why a migrated program behaves exactly like the original: field positions are honored byte-for-byte.

A practical consequence: a sub-structure or a BASED view does not have its own private memory — it shares the buffer of whatever it currently points at. Writing through one view is immediately visible through every other view of the same bytes, just as two PL/I pointers to the same area see each other's changes.


2. Pointers and the 4-byte token

A PL/I POINTER holds an address. The JVM can't store a Java object reference inside a byte buffer, so the runtime stores a 4-byte token in the bytes instead, and keeps the live pointer object in a lookup table:

  • Writing a pointer (POINTER assignment, ADDR(x)) puts a token into the bytes and registers the pointer under that token.
  • Reading a pointer reads the 4-byte token back and looks up the registered pointer.

**The token in the bytes is the stored pointer value.** Two parts of a program that read the same memory read the same token, and therefore resolve to the same pointer — exactly the aliasing PL/I guarantees.

The runtime keeps two registries:

Registry Holds Keyed by
Standalone pointer registry Pointers not embedded in a structure the token
Structure-field registry POINTER fields inside a structure structure + token

You don't manage these directly, but they explain a common trace line: when a pointer is resolved, the runtime is looking the token up in one of these tables. A miss (the token is no longer registered) is the signature of storage that was reclaimed earlier than the program expected.


3. Storage classes = how long memory lives

PL/I storage isn't all equal. What the runtime keeps vs. reclaims follows the storage class of the memory, not a property of the pointer pointing at it:

PL/I / CICS source Storage class Lifetime Runtime scope
EXTRACT EXIT GASET — a Global Work Area region/global lives for the application APPLICATION
GETMAIN SHARED shared storage until an explicit FREEMAIN APPLICATION
plain GETMAIN task storage the task / transaction TRANSACTION
AUTOMATIC, ADDR(local var), working storage stack / per-call the current block / transaction TRANSACTION
  • APPLICATION-scoped memory (a GWA, or GETMAIN SHARED) is genuinely long-lived: it must survive across transactions and is only released when the program explicitly frees it. The runtime keeps it.
  • TRANSACTION-scoped memory is task storage. The runtime reclaims it at the end of the transaction so that long-running, pooled JVM threads don't accumulate dead work areas.

This single rule — retention follows the storage class of the memory — is the heart of how the runtime stays memory-stable under sustained transaction load.

Why this is occasionally subtle

The honest signal for "is this application storage?" is captured when the memory is first allocated/registered, not read live later. A field inside a structure may be created against ordinary task storage and only later be re-pointed at a Global Work Area. If the runtime judged scope by where the field currently points, it would mistake a short-lived duplicate for long-lived storage and keep it forever. By trusting the scope captured at allocation time — plus the explicit "shared" marker that GETMAIN SHARED / EXTRACT EXIT set — the runtime keeps exactly the real Global Work Areas and reclaims the rest.


4. BASED — overlaying a structure on existing memory

A PL/I BASED variable is a declaration that says "I have no storage of my own; read me through this pointer."

DCL 1 ORDER BASED(P),
      3 NEXT POINTER,
      3 DATA CHAR(100);

In the runtime this is the overlay operation: the ORDER template is pointed at the buffer that P addresses. From that moment, reading ORDER.DATA reads the 100 bytes at the pointer's target, through ORDER's layout.

Key points:

  • The same BASED template is re-overlaid on every access, because a BASED reference may legitimately point somewhere new each time (a different list node, a different commarea). The runtime does not cache the target — it re-resolves the pointer so the view is always correct.
  • BASED introduces aliasing: the overlaid structure and the original owner of the buffer are the same bytes. This is intended and matches PL/I.
  • When the pointer's target is APPLICATION-scoped (a GWA), the view inherits that scope — so the structure overlaid on a Global Work Area is correctly treated as long-lived.

5. DEFINED — a fixed overlay at an offset

A PL/I DEFINED variable overlays another variable at a known position:

DCL FULL   CHAR(8);
DCL PART   CHAR(2) DEFINED FULL POSITION(3);   /* bytes 3-4 of FULL */

Where BASED follows a pointer (the target can change), DEFINED is pinned to a fixed offset within a named base. The runtime records the defined position and resolves PART as "the base's buffer, starting at the defined offset." (PL/I positions are 1-based; the runtime translates them to the correct byte offset.)

BASED vs DEFINED at a glance:

BASED DEFINED
Target wherever a pointer currently points a fixed offset in a named base variable
Changes at runtime? yes — re-resolved each access no — the offset is fixed
Typical use list/queue traversal, commareas, dynamic storage reinterpreting part of a record, byte-level overlays

Both share one principle with everything above: no copy is made — the defined/based variable is a window onto the base's bytes.


6. What you'll observe (and how to diagnose it)

Because memory is shared by design, most issues are about a window being open on the wrong bytes, or storage being released too early or kept too long.

Symptom Likely cause
A field reads blank / a screen renders empty A Global Work Area or shared area was treated as transaction storage and reclaimed; the view now points at an empty buffer.
"Buffer too small" / TOOSMALL on a cross-program call A pointer passed through a commarea/parameter was resolved after its storage was reclaimed (a token miss).
Memory grows steadily under load Short-lived work areas are being retained as if application-scoped.
Two variables unexpectedly change together Intended aliasing — they are BASED/DEFINED views of the same bytes.

To diagnose, enable the runtime tracer (see JVM Options & Runtime Configuration):

java -Dheirloom.trace.file=/tmp/trace.csv -jar my-application.jar

The CSV records, for every pointer, whether it was kept or reclaimed and why (its storage scope). A pointer miss on read, paired with a reclaim row for the same token, pinpoints which storage was released before a later program needed it.


7. What each field looks like in the bytes

Within that one buffer, each field occupies a fixed number of bytes at a fixed position, encoded the same way it was on the mainframe — so your record layouts are preserved byte-for-byte. Values are stored big-endian (mainframe order), and fields are packed contiguously (no gaps): a structure's total size is the sum of its fields.

PL/I type Size in the buffer How it's stored
CHARACTER(n) n bytes Character data, space-padded to n
CHARACTER(n) VARYING n bytes Fixed width, space-padded; trailing blanks are trimmed when read
FIXED BINARY 2, 4, or 8 bytes Signed whole number (two's-complement), big-endian; very large precision uses an extended form
FIXED DECIMAL(p,s) ⌈(p+1)/2⌉ bytes Packed decimal — two digits per byte, sign in the last nibble
PICTURE one byte per picture position Display/zoned format following the picture
BIT(n) ⌈n/8⌉ bytes Bits packed left-to-right (IBM bit ordering)
POINTER / OFFSET 4 bytes A token that identifies the target (not a raw machine address)
COMPLEX real + imaginary parts The two parts stored consecutively
nested structure the inner structure's size Laid out inline at the field's position
array element size × number of elements Elements stored back-to-back

Why it matters: because the byte layout matches the original, data read from a file, a commarea, an MQ message, or a database column lands in exactly the positions your program expects — no re-mapping, and no surprises when a record is shared across programs.


Summary

  • A structure is a view (layout + offset); the buffer holds the bytes.
  • A pointer's value is a token in the bytes that resolves to a live pointer.
  • Retention follows the storage class of the memory: Global Work Areas and GETMAIN SHARED are kept (application lifetime); task/transaction storage is reclaimed at the transaction boundary.
  • BASED overlays via a pointer (target can move); DEFINED overlays at a fixed offset. Neither copies — both are windows onto shared bytes.
  • Each field has a fixed size and position, encoded big-endian and packed contiguously, so record layouts are preserved byte-for-byte.

  • JVM Options & Runtime Configuration — how to enable the tracer referenced above.
  • How Transactions Are Managed (ETP + PL/I Runtime)when transaction-scoped storage is reclaimed.
Was this article helpful?
0 out of 0 found this helpful
Have more questions? Submit a request

0 Comments

Article is closed for comments.
Powered by Zendesk