Home · ← SuperJ Manual EN|中文

Runtime Environment Variables

A compiled SuperJ binary carries no configuration file and no command-line runtime flags — it is a plain native executable, and everything it takes from its environment it takes through these variables. They are read by the runtime, not the compiler: they configure a program you have already built, which is the point. A deployed binary can be re-tuned without a rebuild, and the defaults are chosen so that setting none of them is a supportable production configuration.

Compiler flags (--mem-track, --mem-debug, -O3) are a different thing and are documented in build-system.md. Where a variable only does something on a build made with a particular flag, the table says so.

The whole list

VariableDefaultWhat it does
SJ_ARENA_MAX80% of the process's memory budgetHard ceiling on live arena bytes. A breach prints the arena's books and exits 12.
SJ_ARENA_INITIAL2 MiBSize of the global arena's first chunk.
SJ_ARENA_SOLE_MIN1 MiBAllocations at or above this get a chunk of their own.
SUPERJ_STACKTRACEcapture, do not printException stack-trace capture and reporting.
SJ_MEM_LOGoffPath for the memory-event log. --mem-track builds only.
SJ_MEM_SNAPSHOT_EVERY1024 eventsSnapshot cadence in that log. --mem-track builds only.
SJ_MEM_HIST_EVERYoffEmit a memory report every N bytes allocated. --mem-debug builds only.
SJ_INFLATE_MAXthe max array sizeCap on decompressed output, against zip bombs.
SJ_NANOTIMETSC where usableForce System.nanoTime() off the TSC fast path.
SUPERJ_GPUauto-detectSelect the GPU backend.

Size syntax. The four byte-valued arena variables all take the same form: a non-negative integer, an optional k/m/g suffix (case-insensitive), and an optional trailing b. So 128m, 128M, 128mb and 134217728 are the same number. Trailing garbage is rejected rather than partially parsed, so 8zz is a bad value, not 8.

Every one of them is read once, at first use, and cached. Changing the environment of a running process has no effect.


Memory

SJ_ARENA_MAX

A hard ceiling on live arena bytes — the same number System.memReservedBytes() reports and System.memCeilingBytes() reads back.

SJ_ARENA_MAX=512m ./myapp     # fail at 512 MiB
SJ_ARENA_MAX=8g   ./myapp
SJ_ARENA_MAX=0    ./myapp     # no ceiling — run to the host's limit
valueceiling
unset80% of the process's memory budget
512m512 MiB
0none
unparseable (8zz)the default — a typo must not disarm the protection
budget undetectablenone

Why there is a default at all. SuperJ has no garbage collector and no OutOfMemoryError, so an unbounded live set has no termination condition of its own: the process grows until the host thrashes and the OOM killer takes it — no signal, no diagnostic, no exit code, and a host whose page cache has been evicted by one service. Waiting for malloc to fail does not help, because on Linux overcommit hands back a pointer the kernel cannot honour. And a ceiling nobody sets protects nobody: the operator who has never heard of this variable is exactly the one whose process takes the host down. So the knob is the override, not the switch that turns the protection on.

The budget is the host's RAM, or the cgroup limit where one applies, whichever is smaller — a container with a 512 MiB limit on a 64 GiB host gets 410 MiB, not 51 GiB.

The cgroup read follows the chain, not just the root. /proc/self/cgroup on an ordinary Linux box reads 0::/user.slice/user-1000.slice/session-93267.scope, and a container is nested just as deep, so the effective limit is the smallest memory.max from that leaf up to the root — a cap on a parent slice binds exactly as hard as one on the leaf. cgroup v1's memory.limit_in_bytes is the fallback; max and v1's near-LONG_MAX sentinel both mean unlimited and leave the host figure standing.

The 20% headroom is for everything that is not the arena: the malloc domain (String bodies, maps), the stack, the binary's own pages.

On a breach, the process writes its books to stderr and exits 12 (ENOMEM, so a supervisor can tell it from a plain failure). The same summary prints if malloc does fail:

==== SuperJ: out of memory — shutting down (#3209) ====
  cause           : SJ_ARENA_MAX ceiling would be exceeded by this chunk
  request         : 8388608 bytes (one arena chunk)
  ceiling         : 8388608 bytes (SJ_ARENA_MAX)
  arena reserved  : 7340048 bytes  (live, all arenas)
  arena touched   : 7340048 bytes  (100.0% of reserved; 0 bytes of active-chunk slack)
  high water      : 7340048 bytes
  chunks          : 4 live, 4 allocated over the run
  arenas created  : 1
  malloc domain   : 0 bytes in 0 live allocation(s)

The ceiling line says where the number came from — SJ_ARENA_MAX, or 80% of <budget> — because the commonest reader of this summary is someone who did not set it.

Read reserved against touched first: a large gap means the chunk ladder overshot, a small one means the live set really is that big, which is the leak. The breaching arena contributes no slack by construction (we arrive exactly when its active chunk is full), so a single-arena process reads 0 there.

The summary says how much, not where. On a --mem-debug build the per-type and per-site reports print here too, ordered by growth, so the top row names the line (memory-tools.md):

== allocation sites (debug build, SJ_MEM_DEBUG) — out of memory ==
  site                     global_bytes   glob_cnt  ...
  Grow.sj:7                     3316470     195740  ...

Self-monitoring. System.memCeilingBytes() returns the effective ceiling (0 = none), so a service can shed load before it dies rather than after:

long ceiling = System.memCeilingBytes();
if (ceiling > 0 && System.memReservedBytes() > ceiling / 10 * 9) {
    // 90% of the way there — stop accepting work
}

SJ_ARENA_INITIAL

Size of the global arena's first chunk. It starts at 2 MiB and doubles on each expansion up to a 1 GiB cap; a process that already knows it needs multiple GB can skip the ramp's startup doublings.

SJ_ARENA_INITIAL=128m ./myapp
SJ_ARENA_INITIAL=1g   ./myapp     # the cap; larger clamps to it

This is not a pre-fault knob. Chunks are plain malloc and are never pre-touched, so a larger initial chunk removes the startup ramp but does not eliminate page-fault spikes mid-request — RSS tracks touched, not reserved. A bad value falls back to the default.

SJ_ARENA_SOLE_MIN

An allocation at or above this size gets a chunk of its own rather than a slice of the current one, so a single outsized request cannot poison the chunk-growth ramp. Without it, one 10 MB array left the ramp doubled and the next allocation of any size reserved a 20 MB chunk — 3.0× reserved against live on a block doing nothing else.

SJ_ARENA_SOLE_MIN=4m   ./myapp    # only >= 4 MiB gets its own chunk
SJ_ARENA_SOLE_MIN=256k ./myapp    # more allocations get their own chunk

Raising it trades ramp hygiene for fewer chunks; lowering it does the reverse. A bad value falls back to the 1 MiB default rather than disabling the floor.


Diagnostics

SUPERJ_STACKTRACE

Three states, because capture is not free:

valuecaptureprinted in the unhandled-exception report
unsetyesno
1yesyes
0no — getStackTrace() answers emptyno

Measured: 200,000 throw/catch cycles cost 0.42s with capture and 0.015s without — ~2.1 µs per throw against ~0.075 µs. Exceptions mark bugs rather than control flow, so capture-by-default is right; a program that throws in a loop can say 0 without giving up the SDK.

It is not printed by default because a trace's frame count varies with optimization level, so it could never appear in a golden test's compared stderr. printStackTrace(), which you call deliberately, ignores the flag.

SJ_MEM_LOG — --mem-track builds only

Path for the memory-event log; setting it arms the log. Unset means no log even on a --mem-track build, which is what makes the instrumentation free to leave in a shipped binary. See memlog.md.

superj compile myapp.sj --sdk-path $SJ_HOME/sdk --link --mem-track --output myapp
SJ_MEM_LOG=tmp/myapp.mlog ./myapp
superj memlog tmp/myapp.mlog

SJ_MEM_SNAPSHOT_EVERY — --mem-track builds only

How many arena events between SNAPSHOT records in that log (default 1024). Lower it for a short run that would otherwise produce too few snapshots for superj memlog to compute a growth slope — the symptom is insufficient snapshots for slope on a program whose raw CREATE/EXPAND timeline obviously leaks. A non-positive or malformed value falls back to the default.

SJ_MEM_HIST_EVERY — --mem-debug builds only

Emit a full memory report (per-type and per-site tables) every N bytes of global allocation, so a long run produces a growth series without the program being edited. Reports carry a d_global column measured against the previous one and order by it, so the row that is climbing sits above a large-but-flat one.

SJ_MEM_HIST_EVERY=$((64*1024*1024)) ./myapp     # a report every 64 MB allocated

Off unless set. The same report is available on demand from the program via System.memReport(label).


Other

SJ_INFLATE_MAX

Maximum decompressed output size, in bytes, for sj.util.Compression. Defaults to the maximum array size. Lower it when inflating untrusted input — a zip bomb is a small input whose output is the attack. Values above the default, or unparseable ones, are ignored.

SJ_NANOTIME

System.nanoTime() reads the TSC where the CPU reports an invariant one, and falls back to clock_gettime otherwise. Set this to a value beginning with o (as in off) to force the fallback — useful when you suspect TSC calibration rather than your own code.

SUPERJ_GPU

Selects the sj.gpu backend when GpuDevice.open() is called without an explicit one. Unset means auto-detect. Passing a backend name in code is a hard error if unavailable; this variable is the deployment-time equivalent.


See also

SuperJ — manual · generated from runtime-environment.md at pack time EN|中文