Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
5 changes: 3 additions & 2 deletions src/natives/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions src/natives/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down Expand Up @@ -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,

Expand Down
17 changes: 17 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
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;
Expand All @@ -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]);
Expand Down
8 changes: 6 additions & 2 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
const ELEM_SIZE: Record<string, number> = { 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
Expand Down
20 changes: 20 additions & 0 deletions test/heap/Heap.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
12 changes: 12 additions & 0 deletions test/programs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")]);
Expand Down
Loading