2929import java .lang .management .ManagementFactory ;
3030import java .nio .file .Files ;
3131import java .nio .file .Paths ;
32- import java .util .ArrayList ;
3332import java .util .List ;
3433import java .util .function .Function ;
34+ import java .util .function .Supplier ;
3535import java .util .regex .Matcher ;
3636import java .util .regex .Pattern ;
37+ import java .util .stream .Stream ;
3738
3839import com .oracle .svm .core .OS ;
3940import com .oracle .svm .core .SubstrateOptions ;
4041import com .oracle .svm .core .SubstrateUtil ;
42+ import com .oracle .svm .core .util .BasedOnJDKFile ;
4143import com .oracle .svm .core .util .ExitStatus ;
44+ import com .oracle .svm .driver .NativeImage .HostFlags ;
4245import com .oracle .svm .driver .NativeImage .NativeImageError ;
4346
4447import jdk .jfr .internal .JVM ;
48+ import org .graalvm .collections .Pair ;
4549
46- class MemoryUtil {
47- private static final long KiB_TO_BYTES = 1024L ;
48- private static final long MiB_TO_BYTES = 1024L * KiB_TO_BYTES ;
49- private static final long GiB_TO_BYTES = 1024L * MiB_TO_BYTES ;
50+ public final class MemoryUtil {
51+ public static final long KiB_TO_BYTES = 1024L ;
52+ public static final long MiB_TO_BYTES = 1024L * KiB_TO_BYTES ;
53+ public static final long GiB_TO_BYTES = 1024L * MiB_TO_BYTES ;
54+ public static final long TiB_TO_BYTES = 1024L * GiB_TO_BYTES ;
5055
5156 /* Builder needs at least 512MiB for building a helloworld in a reasonable amount of time. */
5257 private static final long MIN_HEAP_BYTES = 512L * MiB_TO_BYTES ;
@@ -61,89 +66,201 @@ class MemoryUtil {
6166 * Builder uses at most 32GB to avoid disabling compressed oops (UseCompressedOops).
6267 * Deliberately use GB (not GiB) to stay well below 32GiB when relative maximum is calculated.
6368 */
64- private static final long MAX_HEAP_BYTES = 32_000_000_000L ;
69+ public static final long MAX_HEAP_BYTES = 32_000_000_000L ;
6570
66- public static List <String > determineMemoryFlags (NativeImage .HostFlags hostFlags ) {
67- List <String > flags = new ArrayList <>();
68- if (hostFlags .hasUseParallelGC ()) {
69- // native image generation is a throughput-oriented task
70- flags .add ("-XX:+UseParallelGC" );
71- }
71+ public static List <String > heuristicMemoryFlags (HostFlags hostFlags , List <String > memoryFlags ) {
7272 /*
7373 * Use MaxRAMPercentage to allow users to overwrite max heap setting with
74- * -XX:MaxRAMPercentage or -Xmx, and freely adjust the min heap with
75- * -XX:InitialRAMPercentage or -Xms.
74+ * -XX:MaxRAMPercentage or -Xmx (though determineMemoryUsageFlags will detect that case and
75+ * not add any flag), and freely adjust the min heap with -XX:InitialRAMPercentage or -Xms.
7676 */
7777 if (hostFlags .hasMaxRAMPercentage ()) {
78- flags . addAll ( determineMemoryUsageFlags (value -> "-XX:MaxRAMPercentage=" + value ) );
78+ return determineMemoryUsageFlags (memoryFlags , value -> "-XX:MaxRAMPercentage=" + value );
7979 } else if (hostFlags .hasMaximumHeapSizePercent ()) {
80- flags .addAll (determineMemoryUsageFlags (value -> "-XX:MaximumHeapSizePercent=" + value .intValue ()));
81- }
82- if (hostFlags .hasGCTimeRatio ()) {
83- /*
84- * Optimize for throughput by increasing the goal of the total time for garbage
85- * collection from 1% to 10% (N=9). This also reduces peak RSS.
86- */
87- flags .add ("-XX:GCTimeRatio=9" ); // 1/(1+N) time for GC
88- }
89- if (hostFlags .hasExitOnOutOfMemoryError ()) {
90- /*
91- * Let builder exit on first OutOfMemoryError to provide for shorter feedback loops.
92- */
93- flags .add ("-XX:+ExitOnOutOfMemoryError" );
80+ return determineMemoryUsageFlags (memoryFlags , value -> "-XX:MaximumHeapSizePercent=" + value .intValue ());
81+ } else {
82+ throw new Error ("Neither -XX:MaxRAMPercentage= nor -XX:MaximumHeapSizePercent= are available" );
9483 }
95- return flags ;
9684 }
9785
86+ // A String in the memory reason to indicate that user memory flags overrode the heuristic
87+ private static final String SET_VIA = ", set via '" ;
88+
9889 /**
9990 * Returns memory usage flags for the build process. Dedicated mode uses a fixed percentage of
10091 * total memory and is the default in containers. Shared mode tries to use available memory to
10192 * reduce memory pressure on the host machine. Note that this method uses OperatingSystemMXBean,
10293 * which is container-aware.
10394 */
104- private static List <String > determineMemoryUsageFlags (Function <Double , String > toMemoryFlag ) {
95+ private static List <String > determineMemoryUsageFlags (List < String > memoryFlags , Function <Double , String > toMemoryFlag ) {
10596 var osBean = (com .sun .management .OperatingSystemMXBean ) ManagementFactory .getOperatingSystemMXBean ();
106- final double totalMemorySize = osBean .getTotalMemorySize ();
107- final double dedicatedMemorySize = totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO ;
108-
109- String memoryUsageReason = "unknown" ;
110- final boolean isDedicatedMemoryUsage ;
111- if (SubstrateUtil .isCISetToTrue ()) {
112- isDedicatedMemoryUsage = true ;
113- memoryUsageReason = "$CI set to 'true'" ;
114- } else if (isContainerized ()) {
115- isDedicatedMemoryUsage = true ;
116- memoryUsageReason = "in container" ;
97+ final long totalMemorySize = osBean .getTotalMemorySize ();
98+
99+ var maxMemoryAndUsageText = maxMemoryHeuristic (totalMemorySize , SubstrateUtil .isCISetToTrue (), isContainerized (), MemoryUtil ::getAvailableMemorySize , memoryFlags );
100+ long maxMemory = maxMemoryAndUsageText .getLeft ();
101+ String memoryUsageText = maxMemoryAndUsageText .getRight ();
102+ String memoryUsageReason = "-D" + SubstrateOptions .BUILD_MEMORY_USAGE_REASON_TEXT_PROPERTY + "=" + memoryUsageText ;
103+
104+ if (memoryUsageText .contains (SET_VIA )) {
105+ return List .of (memoryUsageReason );
117106 } else {
118- isDedicatedMemoryUsage = false ;
107+ double maxRamPercentage = ((double ) maxMemory ) / totalMemorySize * 100.0 ;
108+ String memoryFlag = toMemoryFlag .apply (maxRamPercentage );
109+ return List .of (memoryFlag , memoryUsageReason );
119110 }
111+ }
112+
113+ /**
114+ * Returns the max memory (decided by the heuristic or by the user memory flags) in bytes and
115+ * the reason.
116+ */
117+ public static Pair <Long , String > maxMemoryHeuristic (long totalMemorySize , boolean isCISetToTrue , boolean isContainerized , Supplier <Long > getAvailableMemorySize , List <String > memoryFlags ) {
118+ final long dedicatedMemorySize = (long ) (totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO );
120119
121- double reasonableMaxMemorySize ;
122- if (isDedicatedMemoryUsage ) {
123- reasonableMaxMemorySize = dedicatedMemorySize ;
120+ long maxMemory ;
121+ String reason ;
122+ if (isCISetToTrue ) {
123+ reason = "85% of system memory because $CI set to 'true'" ;
124+ maxMemory = dedicatedMemorySize ;
125+ } else if (isContainerized ) {
126+ reason = "85% of system memory because in container" ;
127+ maxMemory = dedicatedMemorySize ;
124128 } else {
125- reasonableMaxMemorySize = getAvailableMemorySize ();
126- if (reasonableMaxMemorySize >= MIN_AVAILABLE_MEMORY_THRESHOLD_GB * GiB_TO_BYTES ) {
127- memoryUsageReason = "using available memory" ;
129+ long availableMemorySize = getAvailableMemorySize .get ();
130+ if (availableMemorySize >= MIN_AVAILABLE_MEMORY_THRESHOLD_GB * GiB_TO_BYTES ) {
131+ reason = percentageOfSystemMemoryText (availableMemorySize , totalMemorySize ) + ", using all available memory" ;
132+ maxMemory = availableMemorySize ;
128133 } else { // fall back to dedicated mode
129- memoryUsageReason = "less than " + MIN_AVAILABLE_MEMORY_THRESHOLD_GB + "GB of memory available" ;
130- reasonableMaxMemorySize = dedicatedMemorySize ;
134+ reason = "85%% of system memory because less than %dGiB available" . formatted ( MIN_AVAILABLE_MEMORY_THRESHOLD_GB ) ;
135+ maxMemory = dedicatedMemorySize ;
131136 }
132137 }
133138
134- if (reasonableMaxMemorySize < MIN_HEAP_BYTES ) {
139+ if (maxMemory < MIN_HEAP_BYTES ) {
135140 throw new NativeImageError (
136141 "There is not enough memory available on the system (got %sMiB, need at least %sMiB). Consider freeing up memory if builds are slow, for example, by closing applications that you do not need."
137- .formatted (reasonableMaxMemorySize / MiB_TO_BYTES , MIN_HEAP_BYTES / MiB_TO_BYTES ),
142+ .formatted (maxMemory / MiB_TO_BYTES , MIN_HEAP_BYTES / MiB_TO_BYTES ),
138143 null , ExitStatus .OUT_OF_MEMORY .getValue ());
139144 }
140145
141- /* Ensure max memory size does not exceed upper limit. */
142- reasonableMaxMemorySize = Math .min (reasonableMaxMemorySize , MAX_HEAP_BYTES );
146+ // Ensure max memory size does not exceed upper limit
147+ if (maxMemory > MAX_HEAP_BYTES ) {
148+ maxMemory = MAX_HEAP_BYTES ;
149+ reason = percentageOfSystemMemoryText (maxMemory , totalMemorySize ) + ", capped at 32GB" ;
150+ }
151+
152+ // Handle memory flags
153+ if (!memoryFlags .isEmpty ()) {
154+ long newMaxMemory = determineMaxHeapBasedOnMemoryFlags (memoryFlags , maxMemory , totalMemorySize );
155+ if (newMaxMemory > 0 ) {
156+ reason = percentageOfSystemMemoryText (newMaxMemory , totalMemorySize ) + SET_VIA + String .join (" " , memoryFlags ) + "'" ;
157+ maxMemory = newMaxMemory ;
158+ } else {
159+ reason += ", user flags: '%s'" .formatted (String .join (" " , memoryFlags ));
160+ }
161+ }
162+
163+ double maxMemoryGiB = (double ) maxMemory / GiB_TO_BYTES ;
164+ String memoryUsageText = "%.2fGiB of memory (%s)" .formatted (maxMemoryGiB , reason );
165+
166+ return Pair .create (maxMemory , memoryUsageText );
167+ }
168+
169+ static boolean isMemoryFlag (String flag ) {
170+ return Stream .of ("-Xmx" , "-Xms" , "-XX:MaxRAMPercentage=" , "-XX:MaximumHeapSizePercent=" ).anyMatch (flag ::startsWith );
171+ }
172+
173+ @ BasedOnJDKFile ("https://github.com/openjdk/jdk/blob/jdk-26+10/src/hotspot/share/runtime/arguments.cpp#L1530-L1532" )
174+ private static long determineMaxHeapBasedOnMemoryFlags (List <String > memoryFlags , long heuristicMaxMemory , long totalMemory ) {
175+ // Priority: Xmx, MaxRAMPercentage, MaximumHeapSizePercent
176+ var xmx = getMaxMemoryFlagValue ("-Xmx" , memoryFlags , totalMemory );
177+ var maxRAMPercentage = getMaxMemoryFlagValue ("-XX:MaxRAMPercentage=" , memoryFlags , totalMemory );
178+ var maximumHeapSizePercent = getMaxMemoryFlagValue ("-XX:MaximumHeapSizePercent=" , memoryFlags , totalMemory );
179+ var xms = getMaxMemoryFlagValue ("-Xms" , memoryFlags , totalMemory );
180+ long newMaxMemory = 0 ;
181+ if (xmx > 0 ) {
182+ newMaxMemory = xmx ;
183+ } else if (maxRAMPercentage > 0 ) {
184+ newMaxMemory = maxRAMPercentage ;
185+ } else if (maximumHeapSizePercent > 0 ) {
186+ newMaxMemory = maximumHeapSizePercent ;
187+ }
188+
189+ if (newMaxMemory == 0 ? xms > heuristicMaxMemory : xms > newMaxMemory ) {
190+ // Xms only affects max memory if the value is higher than the current max memory value
191+ newMaxMemory = xms ;
192+ }
193+ return newMaxMemory ;
194+ }
195+
196+ private static String percentageOfSystemMemoryText (long maxMemory , long totalMemory ) {
197+ return "%.1f%% of system memory" .formatted (toPercentage (maxMemory , totalMemory ));
198+ }
199+
200+ private static long getMaxMemoryFlagValue (String prefix , List <String > memoryFlags , long totalMemory ) {
201+ long max = 0 ;
202+ for (String flag : memoryFlags ) {
203+ if (flag .startsWith (prefix )) {
204+ long value = parseMemoryFlagValue (flag , totalMemory );
205+ if (value > max ) {
206+ max = value ;
207+ }
208+ }
209+ }
210+ return max ;
211+ }
212+
213+ @ BasedOnJDKFile ("https://github.com/openjdk/jdk/blob/jdk-26+10/src/hotspot/share/utilities/parseInteger.hpp#L105-L160" )
214+ public static long parseMemoryFlagValue (String flag , long totalMemory ) {
215+ if (flag .startsWith ("-Xmx" ) || flag .startsWith ("-Xms" )) {
216+ String valuePart = flag .substring (4 );
217+ if (valuePart .isEmpty ()) {
218+ throw new Error ("Invalid value for: " + flag );
219+ }
220+ char unit = valuePart .charAt (valuePart .length () - 1 );
221+ long multiplier = switch (unit ) {
222+ case 'T' , 't' -> TiB_TO_BYTES ;
223+ case 'G' , 'g' -> GiB_TO_BYTES ;
224+ case 'M' , 'm' -> MiB_TO_BYTES ;
225+ case 'K' , 'k' -> KiB_TO_BYTES ;
226+ default -> 1 ;
227+ };
228+ if (multiplier != 1 ) {
229+ valuePart = valuePart .substring (0 , valuePart .length () - 1 );
230+ }
231+ long value = parseLongOrFlagError (flag , valuePart );
232+ return value * multiplier ;
233+ } else if (flag .startsWith ("-XX:MaxRAMPercentage=" )) {
234+ String valuePart = flag .substring ("-XX:MaxRAMPercentage=" .length ());
235+ double value = parseDoubleOrFlagError (flag , valuePart );
236+ return (long ) (value / 100.0 * totalMemory );
237+ } else if (flag .startsWith ("-XX:MaximumHeapSizePercent=" )) {
238+ String valuePart = flag .substring ("-XX:MaximumHeapSizePercent=" .length ());
239+ double value = parseLongOrFlagError (flag , valuePart );
240+ return (long ) (value / 100.0 * totalMemory );
241+ } else {
242+ throw new Error ("Unknown flag: " + flag );
243+ }
244+ }
245+
246+ private static long parseLongOrFlagError (String flag , String valuePart ) {
247+ try {
248+ return Long .parseLong (valuePart );
249+ } catch (NumberFormatException e ) {
250+ throw new Error ("Invalid value for: " + flag );
251+ }
252+ }
253+
254+ private static double parseDoubleOrFlagError (String flag , String valuePart ) {
255+ try {
256+ return Double .parseDouble (valuePart );
257+ } catch (NumberFormatException e ) {
258+ throw new Error ("Invalid value for: " + flag );
259+ }
260+ }
143261
144- double reasonableMaxRamPercentage = reasonableMaxMemorySize / totalMemorySize * 100 ;
145- return List .of (toMemoryFlag .apply (reasonableMaxRamPercentage ),
146- "-D" + SubstrateOptions .BUILD_MEMORY_USAGE_REASON_TEXT_PROPERTY + "=" + memoryUsageReason );
262+ private static double toPercentage (long part , long total ) {
263+ return part / (double ) total * 100 ;
147264 }
148265
149266 private static boolean isContainerized () {
@@ -153,7 +270,7 @@ private static boolean isContainerized() {
153270 return JVM .isContainerized ();
154271 }
155272
156- private static double getAvailableMemorySize () {
273+ private static long getAvailableMemorySize () {
157274 return switch (OS .getCurrent ()) {
158275 case LINUX -> getAvailableMemorySizeLinux ();
159276 case DARWIN -> getAvailableMemorySizeDarwin ();
0 commit comments