diff --git a/jcstress-core/src/main/java/org/openjdk/jcstress/Options.java b/jcstress-core/src/main/java/org/openjdk/jcstress/Options.java
index f7b9ac08..f8e2ef68 100644
--- a/jcstress-core/src/main/java/org/openjdk/jcstress/Options.java
+++ b/jcstress-core/src/main/java/org/openjdk/jcstress/Options.java
@@ -51,15 +51,14 @@ public class Options {
     private String resultDir;
     private String testFilter;
     private int minStride, maxStride;
-    private int maxFootprint;
     private int time;
     private int iters;
     private final String[] args;
     private boolean parse;
     private boolean list;
     private Verbosity verbosity;
-    private int totalCpuCount;
     private int cpuCount;
+    private int heapPerFork;
     private int forks;
     private String mode;
     private SpinLoopStyle spinStyle;
@@ -107,7 +106,7 @@ public boolean parse() throws IOException {
                 "Reducing the number of CPUs limits the amount of resources (including memory) the run is using.")
                 .withRequiredArg().ofType(Integer.class).describedAs("N");
 
-        OptionSpec<Integer> maxFootprint = parser.accepts("mf", "Maximum footprint for each test, in megabytes. This " +
+        OptionSpec<Integer> heapPerFork = parser.accepts("hs", "Java heap size per fork, in megabytes. This " +
                 "affects the stride size: maximum footprint will never be exceeded, regardless of min/max stride sizes.")
                 .withRequiredArg().ofType(Integer.class).describedAs("MB");
 
@@ -181,7 +180,7 @@ public boolean parse() throws IOException {
             this.verbosity = new Verbosity(0);
         }
 
-        totalCpuCount = VMSupport.figureOutHotCPUs();
+        int totalCpuCount = VMSupport.figureOutHotCPUs();
         cpuCount = orDefault(set.valueOf(cpus), totalCpuCount);
 
         if (cpuCount > totalCpuCount) {
@@ -236,7 +235,7 @@ public boolean parse() throws IOException {
 
         this.minStride = orDefault(set.valueOf(minStride), 10);
         this.maxStride = orDefault(set.valueOf(maxStride), 10000);
-        this.maxFootprint = orDefault(set.valueOf(maxFootprint), 100);
+        this.heapPerFork = orDefault(set.valueOf(heapPerFork), 256);
 
         this.jvmArgs = processArgs(optJvmArgs, set);
         this.jvmArgsPrepend = processArgs(optJvmArgsPrepend, set);
@@ -345,10 +344,6 @@ public int getCPUCount() {
         return cpuCount;
     }
 
-    public int getTotalCPUCount() {
-        return totalCpuCount;
-    }
-
     public String getResultFile() {
         return resultFile;
     }
@@ -361,8 +356,13 @@ public Collection<String> getJvmArgsPrepend() {
         return jvmArgsPrepend;
     }
 
+    public int getHeapPerForkMb() {
+        return heapPerFork;
+    }
+
     public int getMaxFootprintMb() {
-        return maxFootprint;
+        // Half of heap size.
+        return getHeapPerForkMb() / 2;
     }
 
 }
diff --git a/jcstress-core/src/main/java/org/openjdk/jcstress/infra/grading/ReportUtils.java b/jcstress-core/src/main/java/org/openjdk/jcstress/infra/grading/ReportUtils.java
index 2a2f5c13..f835b049 100644
--- a/jcstress-core/src/main/java/org/openjdk/jcstress/infra/grading/ReportUtils.java
+++ b/jcstress-core/src/main/java/org/openjdk/jcstress/infra/grading/ReportUtils.java
@@ -209,11 +209,6 @@ private static boolean skipMessage(String data) {
             return true;
         }
 
-        if (data.contains("Option MaxRAMFraction was deprecated in version") ||
-            data.contains("Option MinRAMFraction was deprecated in version")) {
-            return true;
-        }
-
         if (data.contains("compiler directives added")) {
             return true;
         }
diff --git a/jcstress-core/src/main/java/org/openjdk/jcstress/vm/VMSupport.java b/jcstress-core/src/main/java/org/openjdk/jcstress/vm/VMSupport.java
index 9bbe0b30..a18e0bfb 100644
--- a/jcstress-core/src/main/java/org/openjdk/jcstress/vm/VMSupport.java
+++ b/jcstress-core/src/main/java/org/openjdk/jcstress/vm/VMSupport.java
@@ -72,46 +72,51 @@ public static void initFlags(Options opts) {
                 "-XX:+UnlockDiagnosticVMOptions"
         );
 
-        // Rationale: every test VM uses at least 2 threads. Which means there are at max $CPU/2 VMs.
-        // Reserving half of the RSS of each VM to Java heap leaves enough space for native RSS and
-        // other processes. This means multiplying the factor by 2. These two adjustments cancel each
-        // other.
-        //
-        // It does not matter if user requested lower number of VMs, we still want to follow
-        // the global per-VM fraction. This would trim down the memory requirements along with
-        // CPU requirements.
-        //
-        // Setting -Xms/-Xmx explicitly is supposed to override these defaults.
-        //
-        int part = opts.getTotalCPUCount();
-
-        detect("Trimming down the default VM heap size to 1/" + part + "-th of max RAM",
+        // Tests are supposed to run in a very tight memory constraints:
+        // the test objects are small and reused where possible. The footprint
+        // testing machinery would select appropriate stride sizes to fit the heap.
+        // Users can override this to work on smaller/larger machines, but it should
+        // not be necessary, as even the smallest machines usually have more than 256M
+        // of system memory per CPU.
+
+        int heap = opts.getHeapPerForkMb();
+        detect("Trimming down the VM heap size to " + heap + "M",
                 SimpleTestMain.class,
                 GLOBAL_JVM_FLAGS,
-                "-XX:MaxRAMFraction=" + part, "-XX:MinRAMFraction=" + part);
+                "-Xms" + heap + "M", "-Xmx" + heap + "M");
 
-        detect("Trimming down the number of compiler threads",
-                SimpleTestMain.class,
-                GLOBAL_JVM_FLAGS,
-                "-XX:CICompilerCount=2" // This is the absolute minimum for tiered configurations
-        );
+        // The tests are usually not GC heavy. The minimum amount of threads a jcstress
+        // test uses is 2, so we can expect the CPU affinity machinery to allocate at
+        // least 2 CPUs per fork. This gives us the upper bound for the number of GC threads: 2,
+        // otherwise we risk oversubscribing the forked VM.
+        //
+        // We could, theoretically, drop the number of GC threads to 1, but GC ergonomics
+        // sometimes decides to switch to single-threaded mode in some GC implementations
+        // (e.g. for reference processing), and it would make sense to let GC run in multi-threaded
+        // modes instead.
 
         detect("Trimming down the number of parallel GC threads",
                 SimpleTestMain.class,
                 GLOBAL_JVM_FLAGS,
-                "-XX:ParallelGCThreads=4"
+                "-XX:ParallelGCThreads=2"
         );
 
         detect("Trimming down the number of concurrent GC threads",
                 SimpleTestMain.class,
                 GLOBAL_JVM_FLAGS,
-                "-XX:ConcGCThreads=4"
+                "-XX:ConcGCThreads=2"
         );
 
         detect("Trimming down the number of G1 concurrent refinement GC threads",
                 SimpleTestMain.class,
                 GLOBAL_JVM_FLAGS,
-                "-XX:G1ConcRefinementThreads=4"
+                "-XX:G1ConcRefinementThreads=2"
+        );
+
+        detect("Trimming down the number of compiler threads",
+                SimpleTestMain.class,
+                GLOBAL_JVM_FLAGS,
+                "-XX:CICompilerCount=2" // This is the absolute minimum for tiered configurations
         );
 
         detect("Testing @Contended works on all results and infra objects",