From b2b81bc6b883320e9819b3ec6ed02e36fe8ebb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20Fr=C3=B8lund?= <91736983+jfrolund@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:13:42 +0200 Subject: [PATCH] Cap the auto-sized JVM heap against total device RAM The auto path sizes the heap purely from free memory: long allocatedRam = Math.max(availMem, 1536); That is a floor with no ceiling, so the more RAM is free at launch the larger a heap the game claims for the rest of the session. 874e85b ("Fix ram setter") introduced this when it replaced the previous bounded calculation, which did cap the result. The relationship is backwards on a headset. The heap is only part of the process -- the GL driver, XR swapchains, LWJGL and the JRE need roughly as much again outside it -- and the VR compositor and system services have to keep running or lmkd kills the game. Measured on a 6GB Quest 2: launching with ~3.5GB free produced a 2048MB heap and a ~3.9GB process that was killed with reason=3 (LOW_MEMORY) repeatedly, while the same device launching with only ~322MB free took the 1536MB floor and survived. Having more memory free made the game less stable, not more. Clamp the result to a quarter of total RAM. That scales with the device rather than hardcoding a number, and it makes the heap stable across launches instead of varying with whatever else happened to be running. Also lower the startup commitment so the heap grows only as the game needs it, and stop pinning -Xms to -Xmx on the custom path, where it forced the user's entire chosen value to be committed immediately. Quest 2 goes from unbounded (2048MB+ observed) to 1458MB; Quest 3 to 1950MB; a 12GB Quest Pro to 2925MB. --- src/main/java/pojlib/util/JREUtils.java | 38 +++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/pojlib/util/JREUtils.java b/src/main/java/pojlib/util/JREUtils.java index c5b582b8..13013119 100644 --- a/src/main/java/pojlib/util/JREUtils.java +++ b/src/main/java/pojlib/util/JREUtils.java @@ -40,6 +40,21 @@ private JREUtils() {} private static String sNativeLibDir; private static String runtimeDir; + /** Smallest heap we will hand out, so the game can still start on a busy device. */ + private static final long MIN_HEAP_MB = 1024; + /** + * Largest share of the device's total RAM the Java heap may take. + *

+ * The heap is only one part of the process: the GL driver, the XR swapchains, LWJGL and the + * JRE itself need roughly as much again outside it, and the VR compositor and system services + * have to keep running or the low memory killer takes the game out. Sizing purely from free + * memory means a device that happens to launch with a lot free hands out a heap it cannot + * afford for the rest of the session. + */ + private static final double MAX_HEAP_FRACTION = 0.25; + /** Heap committed at startup; the JVM grows towards -Xmx only as the game actually needs it. */ + private static final long INITIAL_HEAP_MB = 512; + public static String findInLdLibPath(String libName) { if(Os.getenv("LD_LIBRARY_PATH")==null) { try { @@ -210,6 +225,20 @@ public static boolean prelaunchCheck(Activity activity, MinecraftInstances.Insta return true; } + /** + * Picks a max heap size that fits alongside everything else the process and the headset need. + * + * @param availMem memory currently free to the app, in MB + * @param totalMem the device's total RAM in MB + * @return the heap size to pass to -Xmx, in MB + */ + static long clampHeap(long availMem, long totalMem) { + long ceiling = (long) (totalMem * MAX_HEAP_FRACTION); + // Never return less than the floor, even on a device small enough that the ceiling + // would fall below it, or the game cannot start at all. + return Math.max(MIN_HEAP_MB, Math.min(availMem, ceiling)); + } + public static int launchJavaVM(final Activity activity, final List JVMArgs, MinecraftInstances.Instance instance) throws Throwable { final String graphicsLib = loadGraphicsLibrary(); List userArgs = getJavaArgs(activity, instance); @@ -217,18 +246,21 @@ public static int launchJavaVM(final Activity activity, final List JVMAr //Add automatically generated args if (API.customRAMValue) { Logger.getInstance().appendToLog("Setting JVM memory to " + API.memoryValue + "MB (Custom)"); - userArgs.add("-Xms" + API.memoryValue + "M"); + // Start small and let the heap grow into the value the user picked. Committing it all + // up front costs that memory even in sessions that never need it. + userArgs.add("-Xms" + INITIAL_HEAP_MB + "M"); userArgs.add("-Xmx" + API.memoryValue + "M"); } else { ActivityManager manager = (ActivityManager) activity.getSystemService(Activity.ACTIVITY_SERVICE); ActivityManager.MemoryInfo ami = new ActivityManager.MemoryInfo(); manager.getMemoryInfo(ami); long availMem = (ami.availMem-ami.threshold)/(1024*1024); - long allocatedRam = Math.max(availMem, 1536); + long totalMem = ami.totalMem/(1024*1024); + long allocatedRam = clampHeap(availMem, totalMem); Logger.getInstance().appendToLog("Setting JVM memory to " + allocatedRam + "MB"); - userArgs.add("-Xms" + 1024 + "M"); + userArgs.add("-Xms" + Math.min(INITIAL_HEAP_MB, allocatedRam) + "M"); userArgs.add("-Xmx" + allocatedRam + "M"); }