From 0e7ffc5368a9558edf1a5775111b7122ca7a0a0a Mon Sep 17 00:00:00 2001 From: Ralf Schmelter Date: Tue, 16 Jun 2026 14:56:15 +0200 Subject: [PATCH 1/2] Add heap dump features for buildpack. --- src/hotspot/share/runtime/globals.hpp | 32 +++ src/hotspot/share/services/heapDumper.cpp | 51 +++- .../HeapDump/PartialArrayContentTest.java | 268 ++++++++++++++++++ 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java diff --git a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp index 081a2804f15a..31a3570bf43b 100644 --- a/src/hotspot/share/runtime/globals.hpp +++ b/src/hotspot/share/runtime/globals.hpp @@ -711,6 +711,38 @@ const int ObjectAlignmentInBytes = 8; "compression. Otherwise the level must be between 1 and 9.") \ range(0, 9) \ \ +/* SapMachine 2026-06-16: Allow to overwrite the heap dump file. */ \ + product(bool, HeapDumpOverwrite, false, MANAGEABLE, \ + "If enabled, the heap dump on out of memory error can " \ + "overwrite an already existing file.") \ + \ + /* SapMachine 2026-06-16: For upward compatibility. */ \ + product(uint, HeapDumpParallelism, 0, MANAGEABLE, \ + "This is ignored.") \ + range(0, 100000) \ + \ + /* SapMachine 2026-06-16: Allow to skip content of arrays in dumps.*/ \ + product(bool, LimitPrimitiveArrayContentInHeapDump, false, MANAGEABLE, \ + "If enabled, the content of primitive arrays is not completely " \ + "written to a heap dump for large arrays. Note that this only " \ + "really saves space, if the compression of the heap dump is " \ + "enabled too, since the skipped elements are written as " \ + "0 or false.") \ + \ + /* SapMachine 2026-06-16: Allow to skip content of arrays in dumps.*/ \ + product(int, StringLikeContentSizeLimitInHeapDump, 120, MANAGEABLE, \ + "The number of entries in primitive char and byte arrays to " \ + "not skip in a heap dump when " \ + "LimitPrimitiveArrayContentInHeapDump is enabled.") \ + range(0, 100000) \ + \ + /* SapMachine 2026-06-16: Allow to skip contents of arrays in dumps.*/ \ + product(int, ArrayContentSizeLimitInHeapDump, 50, MANAGEABLE, \ + "The number of entries in a primitive array other than char and " \ + "byte arrays to not skip in a heap dump when " \ + "LimitPrimitiveArrayContentInHeapDump is enabled.") \ + range(0, 100000) \ + \ product(ccstr, NativeMemoryTracking, DEBUG_ONLY("summary") NOT_DEBUG("off"), \ "Native memory tracking options") \ \ diff --git a/src/hotspot/share/services/heapDumper.cpp b/src/hotspot/share/services/heapDumper.cpp index 0e71a40948f0..58550f18ce05 100644 --- a/src/hotspot/share/services/heapDumper.cpp +++ b/src/hotspot/share/services/heapDumper.cpp @@ -436,6 +436,8 @@ class AbstractDumpWriter : public StackObj { void write_symbolID(Symbol* o); void write_classID(Klass* k); void write_id(u4 x); + // SapMachine 2026-06-16: Writes zeros to the buffer. + void write_zero(size_t len); // Start a new sub-record. Starts a new heap dump segment if needed. void start_sub_record(u1 tag, u4 len); @@ -543,6 +545,26 @@ void AbstractDumpWriter::write_id(u4 x) { #endif } +// SapMachine 2026-06-16: Writes zeros to the buffer. +void AbstractDumpWriter::write_zero(size_t len) { + assert(!_in_dump_segment || (_sub_record_left >= len), "sub-record too large"); + DEBUG_ONLY(_sub_record_left -= len); + + // flush buffer to make room. + while (len > buffer_size() - position()) { + assert(!_in_dump_segment || _is_huge_sub_record, + "Cannot overflow in non-huge sub-record."); + size_t to_write = buffer_size() - position(); + memset(buffer() + position(), 0, to_write); + len -= to_write; + set_position(position() + to_write); + flush(); + } + + memset(buffer() + position(), 0, len); + set_position(position() + len); +} + // We use java mirror as the class ID void AbstractDumpWriter::write_classID(Klass* k) { write_objectID(k->java_mirror()); @@ -1531,6 +1553,24 @@ void DumperSupport::dump_prim_array(AbstractDumpWriter* writer, typeArrayOop arr return; } + // SapMachine 2026-06-16: If enabled, we don't dump the whole content of large arrays, but just the start + // and fill the rest with zeroes. + int fill_with_zero = 0; + + if (LimitPrimitiveArrayContentInHeapDump) { + int limit = ArrayContentSizeLimitInHeapDump; + + if (type == T_BYTE || type == T_CHAR) { + limit = StringLikeContentSizeLimitInHeapDump; + } + + if (length > limit) { + fill_with_zero = length - limit; + length = limit; + length_in_bytes = (u4)length * type_size; + } + } + // If the byte ordering is big endian then we can copy most types directly switch (type) { @@ -1598,6 +1638,11 @@ void DumperSupport::dump_prim_array(AbstractDumpWriter* writer, typeArrayOop arr default : ShouldNotReachHere(); } + // SapMachine 2026-06-16: Fill with zeros, if we don't dump the whole content of the array. + if (fill_with_zero > 0) { + writer->write_zero((u4)fill_with_zero * type_size); + } + writer->end_sub_record(); } @@ -2621,7 +2666,8 @@ void HeapDumper::set_error(char const* error) { // outside of a JVM safepoint void HeapDumper::dump_heap_from_oome() { // SapMachine 2024-05-10: HeapDumpPath for jcmd - HeapDumper::dump_heap(false, true); + // SapMachine 2026-06-16: Handle HeapDumpOverwrite + HeapDumper::dump_heap(false, true, tty, -1, HeapDumpOverwrite); } // Called by error reporting by a single Java thread outside of a JVM safepoint, @@ -2631,7 +2677,8 @@ void HeapDumper::dump_heap_from_oome() { // inteference when updating the static variables base_path and dump_file_seq below. void HeapDumper::dump_heap() { // SapMachine 2024-05-10: HeapDumpPath for jcmd - HeapDumper::dump_heap(false, false); + // SapMachine 2026-06-16: Handle HeapDumpOverwrite + HeapDumper::dump_heap(false, false, tty, -1, HeapDumpOverwrite); } // SapMachine 2024-05-10: HeapDumpPath for jcmd diff --git a/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java b/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java new file mode 100644 index 000000000000..4eb6bb7ae7d4 --- /dev/null +++ b/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2026 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; + +import jdk.test.lib.Asserts; +import jdk.test.lib.JDKToolLauncher; +import jdk.test.lib.apps.LingeredApp; +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.hprof.model.*; +import jdk.test.lib.hprof.parser.Reader; + +/* + * @test + * @summary Checks if -XX:+LimitPrimArrayContentInHeapDump works. + * @library /test/lib + * @run driver PartialArrayContentTest + */ +class ArrayAllocApp extends LingeredApp { + public static int arraySize = 54321; + + public static boolean[] za = new boolean[arraySize]; + public static byte[] ba = new byte[arraySize]; + public static short[] sa = new short[arraySize]; + public static char[] ca = new char[arraySize]; + public static int[] ia = new int[arraySize]; + public static long[] ja = new long[arraySize]; + public static float[] fa = new float[arraySize]; + public static double[] da = new double[arraySize]; + + public static void allocArrays() { + for (int i = 0; i < arraySize; ++i) { + za[i] = true; + ba[i] = (byte) 1; + sa[i] = (short) 1; + ca[i] = '1'; + ia[i] = 1; + ja[i] = 1; + fa[i] = 1.0f; + da[i] = 1.0; + } + } + public static void main(String[] args) { + allocArrays(); + LingeredApp.main(args); + } +} + +class ArrayAllocOOMApp extends ArrayAllocApp { + // The size of the short array to be slightly larger than 2 GB. + public static int largestArraySize = Integer.MAX_VALUE / 2 + 100; + public static short[] largeArray; + + public static void main(String[] args) { + allocArrays(); + largeArray = new short[largestArraySize]; + byte[] b = new byte[largestArraySize]; + } +} + +public class PartialArrayContentTest { + private static int charLikeLimit = 120; + private static int nonCharLikeLimit = 50; + + public static void main(String[] args) throws Exception { + checkPartialContentWithJcmd(); + checkPartialContentWithOOM(); + } + + public static void checkPartialContentWithJcmd() throws Exception { + File dumpFile = new File("partialarrays_with_jcmd.hprof"); + createDump(dumpFile, true, + "-Xmx500M", + "-XX:+LimitPrimitiveArrayContentInHeapDump", + "-XX:StringLikeContentSizeLimitInHeapDump=" + charLikeLimit, + "-XX:ArrayContentSizeLimitInHeapDump=" + nonCharLikeLimit); + verifyDump(dumpFile, true); + } + + public static void checkPartialContentWithOOM() throws Exception { + // Check -XX:+HeapDumpOverwrite and -XX:HeapDumpParallelism too. + File dumpFile = new File("partialarrays_with_oom.hprof"); + FileOutputStream fos = new FileOutputStream(dumpFile); + fos.close(); + createDump(dumpFile, false, + "-Xmx500M", + "-XX:+HeapDumpOnOutOfMemoryError", + "-XX:HeapDumpPath=" + dumpFile, + "-XX:+HeapDumpOverwrite", + "-XX:HeapDumpParallelism=1"); + verifyDump(dumpFile, false); + Asserts.assertEquals(1L, countTags(dumpFile, 0x2c), "Must have on end segmemnt"); + // Create a dump with an array > 2GB to check for integer overflows in the partial array code. + createDump(dumpFile, false, + "-Xmx2500M", // Ensures the 2 billion entry short array is in the heap dump. + "-XX:+HeapDumpOnOutOfMemoryError", + "-XX:HeapDumpPath=" + dumpFile, + "-XX:+HeapDumpOverwrite", + "-XX:+LimitPrimitiveArrayContentInHeapDump"); + int largestArraySize = verifyDump(dumpFile, true); + Asserts.assertEquals(largestArraySize, ArrayAllocOOMApp.largestArraySize); + } + + private static void createDump(File dumpFile, boolean useJcmd, String... vmArgs) throws Exception { + LingeredApp theApp = null; + try { + theApp = useJcmd ? new ArrayAllocApp() : new ArrayAllocOOMApp(); + LingeredApp.startApp(theApp, vmArgs); + Asserts.assertTrue(useJcmd, "startApp() should throw when OOM."); + + //jcmd GC.heap_dump + JDKToolLauncher launcher = JDKToolLauncher + .createUsingTestJDK("jcmd") + .addToolArg(Long.toString(theApp.getPid())) + .addToolArg("GC.heap_dump") + .addToolArg(dumpFile.getAbsolutePath()); + Process p = ProcessTools.startProcess("jcmd", new ProcessBuilder(launcher.getCommand())); + + while (!p.waitFor(5, TimeUnit.SECONDS)) { + if (!theApp.getProcess().isAlive()) { + p.destroyForcibly(); + throw new Exception("Target VM died"); + } + } + + Asserts.assertEquals(p.exitValue(), 0); + } catch (IOException e) { + Asserts.assertFalse(useJcmd, "We expect to fail when throwing OOM."); + } finally { + if (useJcmd) { + LingeredApp.stopApp(theApp); + } + } + } + + private static long countTags(File dumpFile, int tagToCount) throws Exception { + long result = 0; + + try (RandomAccessFile raf = new RandomAccessFile(dumpFile, "r")) { + raf.skipBytes("JAVA PROFILE 1.0.2".length() + 1 + 4 + 8); // Skip header plus \0, size of oops and time stamp. + + while (true) { + int tag = raf.readUnsignedByte(); + Asserts.assertTrue(tag <= 0x2c, "Unknown tag " + tag); + + if (tag == tagToCount) { + result += 1; + } + + raf.skipBytes(4); + long size = raf.readInt() & 0xffffffffL; + raf.seek(raf.getFilePointer() + size); + } + } catch (EOFException e) { + // Expected. + } + + return result; + } + + private static int verifyDump(File dumpFile, boolean isPartial) throws Exception { + Asserts.assertTrue(dumpFile.exists(), "Heap dump file not found."); + int largestArraySize = 0; + + try (Snapshot snapshot = Reader.readFile(dumpFile.getPath(), true, 0)) { + snapshot.resolve(true); + Enumeration things = snapshot.getThings(); + HashSet expectedTypes = new HashSet<>(); + expectedTypes.add('Z'); + expectedTypes.add('B'); + expectedTypes.add('S'); + expectedTypes.add('C'); + expectedTypes.add('I'); + expectedTypes.add('J'); + expectedTypes.add('F'); + expectedTypes.add('D'); + + while (things.hasMoreElements()) { + JavaHeapObject obj = things.nextElement(); + + if (obj instanceof JavaValueArray) { + JavaValueArray array = (JavaValueArray) obj; + + largestArraySize = Math.max(largestArraySize, array.getLength()); + + if (array.getLength() != ArrayAllocApp.arraySize) { + continue; + } + + char type = (char) array.getElementType(); + Asserts.assertTrue(expectedTypes.remove(type)); + int limit = ((type == 'B') || (type == 'C')) ? charLikeLimit : nonCharLikeLimit; + JavaThing[] values = array.getElements(); + + String exp1 = ""; + String exp2 = ""; + + switch (type) { + case 'Z': + exp1 = "true"; + exp2 = "false"; + break; + case 'B': + exp1 = "0x1"; + exp2 = "0x0"; + break; + case 'S': + case 'I': + case 'J': + exp1 = "1"; + exp2 = "0"; + break; + case 'C': + exp1 = "1"; + exp2 = "" + (char) 0; + break; + case 'F': + case 'D': + exp1 = "1.0"; + exp2 = "0.0"; + break; + } + + int fullPart = isPartial ? limit : values.length; + + for (int i = 0; i < fullPart; ++i) { + Asserts.assertEquals(exp1, values[i].toString()); + } + + for (int i = fullPart; i < ArrayAllocApp.arraySize; ++i) { + Asserts.assertEquals(exp2, values[i].toString()); + } + } + } + + Asserts.assertTrue(expectedTypes.isEmpty()); + } + + return largestArraySize; + } +} From 4540de90328fe0c131cdccddfcca9b38b26ab56b Mon Sep 17 00:00:00 2001 From: Ralf Schmelter Date: Wed, 17 Jun 2026 12:51:12 +0200 Subject: [PATCH 2/2] Test improvements. --- .../serviceability/HeapDump/PartialArrayContentTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java b/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java index 4eb6bb7ae7d4..75f19c18dd82 100644 --- a/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java +++ b/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java @@ -39,7 +39,8 @@ /* * @test - * @summary Checks if -XX:+LimitPrimArrayContentInHeapDump works. + * @summary Checks if -XX:+LimitPrimitiveArrayContentInHeapDump works. + * @requires os.maxMemory > 4G * @library /test/lib * @run driver PartialArrayContentTest */ @@ -116,7 +117,7 @@ public static void checkPartialContentWithOOM() throws Exception { "-XX:+HeapDumpOverwrite", "-XX:HeapDumpParallelism=1"); verifyDump(dumpFile, false); - Asserts.assertEquals(1L, countTags(dumpFile, 0x2c), "Must have on end segmemnt"); + Asserts.assertEquals(1L, countTags(dumpFile, 0x2c), "Must have one end segment"); // Create a dump with an array > 2GB to check for integer overflows in the partial array code. createDump(dumpFile, false, "-Xmx2500M", // Ensures the 2 billion entry short array is in the heap dump.