diff --git a/docs/node.md b/docs/node.md index 73faa60..d1b7bb7 100644 --- a/docs/node.md +++ b/docs/node.md @@ -19,6 +19,7 @@ npx @project516/java-web [options] -jar app.jar [args] | `--enable-preview` | preview features | | `--add-exports`, `--add-opens`, `--add-reads`, `--add-modules`, `--module-path`, `-p` | module options, as with `java` | | `--locale-data` | locales other than English (see [support.md](support.md#locales)) | +| `-Xmx` | the largest array, and what `Runtime.maxMemory()` reports (default and at most 1g, rounded up to 2 MB steps, below 2m an error); the total heap is not counted | | `--max-rss MB` | exit when resident memory passes this (default 2048) | | `--jdk DIR` | an exploded JDK image instead of the packaged one | | `--debug` | log VM events | diff --git a/src/natives/lang.ts b/src/natives/lang.ts index 0df48ba..d47c054 100644 --- a/src/natives/lang.ts +++ b/src/natives/lang.ts @@ -315,8 +315,9 @@ export const natives: Natives = { // Runtime "java/lang/Runtime.availableProcessors()I": () => 1, - "java/lang/Runtime.freeMemory()J": () => 256n << 20n, - "java/lang/Runtime.totalMemory()J": () => 512n << 20n, + // free <= total <= max, as callers that compute available memory expect + "java/lang/Runtime.freeMemory()J": () => BigInt(Math.min(512 << 20, MAX_HEAP) / 2), + "java/lang/Runtime.totalMemory()J": () => BigInt(Math.min(512 << 20, MAX_HEAP)), "java/lang/Runtime.maxMemory()J": () => BigInt(MAX_HEAP), // Collects where the engine allows it (node --expose-gc), in a new JS job: // a WeakRef read earlier in a job keeps its target alive until the job ends. diff --git a/src/natives/management.ts b/src/natives/management.ts index 0bd5a70..3f4acfc 100644 --- a/src/natives/management.ts +++ b/src/natives/management.ts @@ -91,8 +91,14 @@ function* poolUsage(t: JThread, self: JObject): NativeGen { const b: Beans = yield* beans(t); const pool = POOLS[b.pools.indexOf(self)]; if (pool.name !== OLD) return yield* usage(t, 0, 0, 0, -1); + return yield* heapUsage(t); +} + +// MemoryUsage needs used <= committed <= max; node's heap does not know -Xmx +function* heapUsage(t: JThread): NativeGen { const h = jsHeap(); - return yield* usage(t, 0, h.used, Math.max(h.used, h.committed), MAX_HEAP); + const committed = Math.min(Math.max(h.used, h.committed), MAX_HEAP); + return yield* usage(t, 0, Math.min(h.used, committed), committed, MAX_HEAP); } function liveThreads(vm: JVM): JThread[] { @@ -146,8 +152,7 @@ export const natives: Natives = { }, "sun/management/MemoryImpl.getMemoryUsage0(Z)Ljava/lang/management/MemoryUsage;": function* (t, [, heap]): NativeGen { if (!heap) return yield* usage(t, 0, 0, 0, -1); - const h = jsHeap(); - return yield* usage(t, 0, h.used, Math.max(h.used, h.committed), MAX_HEAP); + return yield* heapUsage(t); }, "sun/management/MemoryImpl.setVerboseGC(Z)V": () => undefined, diff --git a/src/node.ts b/src/node.ts index 39099d3..67a84b0 100644 --- a/src/node.ts +++ b/src/node.ts @@ -15,6 +15,7 @@ import { hostNatives } from "./hostnatives.ts"; import { ImageBackend } from "./image.ts"; import { NodeNet } from "./net-node.ts"; import { PendingThrow } from "./thread.ts"; +import { setMaxHeap } from "./runtime.ts"; import type { NetBackend } from "./sockets.ts"; // A host directory exposed read-only (or writable) inside the VFS. @@ -314,6 +315,12 @@ export function moduleFlag(o: NodeRunOptions, arg: string, next: string | undefi return true; } +// a -Xmx size: bytes, or k, m or g +function heapSize(s: string): number { + const unit = { k: 2 ** 10, m: 2 ** 20, g: 2 ** 30 }[s.slice(-1).toLowerCase()]; + return unit ? Number(s.slice(0, -1)) * unit : Number(s); +} + function main(argv: string[]) { const o: NodeRunOptions = { classpath: ["."], props: {} }; let i = 0; @@ -329,6 +336,16 @@ function main(argv: string[]) { else if (isAssertionFlag(a)) (o.assertions ??= []).push(a); else if (a === "--enable-preview") o.enablePreview = true; else if (a === "--locale-data") o.localeData = true; + else if (a.startsWith("-Xmx")) { + // x64 HotSpot refuses to start below 2 MB and rounds up to 2 MB steps + const n = /^-Xmx\d+[kmg]?$/i.test(a) ? heapSize(a.slice(4)) : 0; + if (n < 2 ** 21) { + console.error(n === 0 ? `Invalid maximum heap size: ${a}\nError: Could not create the Java Virtual Machine.\nError: A fatal exception has occurred. Program will exit.` + : "Error occurred during initialization of VM\nToo small maximum heap"); + process.exit(1); + } + setMaxHeap(Math.ceil(n / 2 ** 21) * 2 ** 21); + } else if (/^(-X|--add-|-verbose|-server|-client)/.test(a)) continue; else if (a === "--jdk") o.jdk = argv[++i]; else if (a === "--max-rss") o.maxRssMb = Number(argv[++i]); diff --git a/src/runtime.ts b/src/runtime.ts index dfd6850..38f40f5 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -359,8 +359,12 @@ export function primName(c: string) { throw new Error(`not a primitive: ${c}`); } -// Reported by Runtime.maxMemory; no array may be bigger. -export const MAX_HEAP = 1 << 30; +// Reported by Runtime.maxMemory; no array may be bigger. -Xmx lowers it; the +// total of live objects is not counted. +export let MAX_HEAP = 1 << 30; +export function setMaxHeap(bytes: number) { + MAX_HEAP = Math.min(bytes, 1 << 30); +} const ELEM_SIZE: Record = { Z: 1, B: 1, C: 2, S: 2, I: 4, F: 4, J: 8, D: 8, L: 4 }; // V8 gives a typed array over 64 bytes its own malloc'd store, about ten diff --git a/test/heap/Heap.java b/test/heap/Heap.java new file mode 100644 index 0000000..3d01e9f --- /dev/null +++ b/test/heap/Heap.java @@ -0,0 +1,20 @@ +// Run with -Xmx17m: an array bigger than the heap fails, a small one does not. +// With an argument, prints the heap's size. +public class Heap { + public static void main(String[] args) { + try { + int[] big = new int[8 << 20]; + System.out.println("allocated " + big.length); + } catch (OutOfMemoryError e) { + System.out.println("OutOfMemoryError: " + e.getMessage()); + } + System.out.println("small " + new int[1 << 20].length); + Runtime rt = Runtime.getRuntime(); + System.out.println("free <= total <= max " + (rt.freeMemory() <= rt.totalMemory() && rt.totalMemory() <= rt.maxMemory())); + var usage = java.lang.management.ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); + System.out.println("heap usage within max " + (usage.getCommitted() <= usage.getMax() && usage.getMax() == rt.maxMemory())); + // the heap's exact size depends on the platform's rounding + long max = rt.maxMemory(); + System.out.println(args.length > 0 ? "max " + max : "max under 32 MB " + (max <= 32 << 20)); + } +} diff --git a/test/programs.test.ts b/test/programs.test.ts index 54dcc2d..d092ae3 100644 --- a/test/programs.test.ts +++ b/test/programs.test.ts @@ -64,6 +64,18 @@ test("System.gc collects under the command", { skip: !files.includes("Refs.java" assert.deepEqual({ stdout: got.stdout, code: got.status }, { stdout: ref.stdout, code: ref.status }); }); +test("-Xmx caps an allocation under the command", () => { + const classes = join(root, "build", "test-heap"); + execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "heap", "Heap.java")]); + const args = ["-Xmx17m", "-cp", classes, "Heap"]; + const ref = spawnSync(hotspot("java"), args, { encoding: "utf8" }); + const got = spawnSync(process.execPath, [join(root, "src", "node.ts"), ...args], { encoding: "utf8" }); + assert.deepEqual({ stdout: got.stdout, code: got.status }, { stdout: ref.stdout, code: ref.status }); + // x64 HotSpot rounds the heap up to 2 MB steps + const max = spawnSync(process.execPath, [join(root, "src", "node.ts"), ...args, "max"], { encoding: "utf8" }); + assert.match(max.stdout, /^max 18874368$/m); +}); + test("stdout closed by its reader", async () => { const classes = join(root, "build", "test-pipe"); execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "pipe", "Pipe.java")]);