diff --git a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp index 27f44314d01..d94fa0f1778 100644 --- a/src/hotspot/share/runtime/globals.hpp +++ b/src/hotspot/share/runtime/globals.hpp @@ -716,6 +716,38 @@ const int ObjectAlignmentInBytes = 8; "compression. Otherwise the level must be between 1 and 9.") \ range(0, 9) \ \ + /* SapMachine 2026-05-06: 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-05-06: Sets the parallelism of the heap dump. */ \ + product(uint, HeapDumpParallelism, 0, MANAGEABLE, \ + "Sets the parallelism of the heap dump creation. 0 means to let "\ + "the VM decide.") \ + \ + /* SapMachine 2026-05-06: 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-05-06: 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-05-06: 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 ca25e7387ad..d6836648655 100644 --- a/src/hotspot/share/services/heapDumper.cpp +++ b/src/hotspot/share/services/heapDumper.cpp @@ -441,6 +441,8 @@ class AbstractDumpWriter : public CHeapObj { void write_symbolID(Symbol* o); void write_classID(Klass* k); void write_id(u4 x); + // SapMachine 2026-05-06: 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); @@ -539,6 +541,26 @@ void AbstractDumpWriter::write_id(u4 x) { #endif } +// SapMachine 2026-05-06: 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()); @@ -1357,6 +1379,8 @@ void DumperSupport::dump_prim_array(AbstractDumpWriter* writer, typeArrayOop arr int length = calculate_array_max_length(writer, array, header_size); int type_size = type2aelembytes(type); + // SapMachine 2026-05-06 + int fill_with_zero = 0; u4 length_in_bytes = (u4)length * type_size; u4 size = header_size + length_in_bytes; @@ -1372,6 +1396,21 @@ void DumperSupport::dump_prim_array(AbstractDumpWriter* writer, typeArrayOop arr return; } + // SapMachine 2026-05-06: If enabled, we don't dump the whole content of large arrays, but just the start. + 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) { @@ -1439,6 +1478,11 @@ void DumperSupport::dump_prim_array(AbstractDumpWriter* writer, typeArrayOop arr default : ShouldNotReachHere(); } + // SapMachine 2026-05-06: 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(); } @@ -2200,6 +2244,11 @@ void DumpMerger::merge_file(const char* path) { #endif void DumpMerger::do_merge() { + // SapMachine 2026-05-06: No need to merge a non-parallel heap dump. + if (_dump_seq <= 1) { + return; + } + TraceTime timer("Merge heap files complete", TRACETIME_LOG(Info, heapdump)); // Since contents in segmented heap file were already zipped, we don't need to zip @@ -2475,8 +2524,11 @@ void VM_HeapDumper::work(uint worker_id) { ResourceMark rm; // share global compressor, local DumpWriter is not responsible for its life cycle - DumpWriter segment_writer(DumpMerger::get_writer_path(writer()->get_file_path(), dumper_id), - writer()->is_overwrite(), writer()->compressor()); + // SapMachine 2026-05-06: Don't use segments if the dump is not parallel. This makes it + // possible to not use any disk space if dumping to a names pipe or a tty. + DumpWriter* parallel_writer = is_parallel_dump() ? new DumpWriter(DumpMerger::get_writer_path(writer()->get_file_path(), dumper_id), + writer()->is_overwrite(), writer()->compressor()) : nullptr; + DumpWriter& segment_writer = parallel_writer == nullptr ? *writer() : *parallel_writer; if (!segment_writer.has_error()) { if (is_vm_dumper(dumper_id)) { // dump some non-heap subrecords to heap dump segment @@ -2533,6 +2585,8 @@ void VM_HeapDumper::work(uint worker_id) { // At this point, all fragments of the heapdump have been written to separate files. // We need to merge them into a complete heapdump and write HPROF_HEAP_DUMP_END at that time. } + // SapMachine 2026-05-06 + delete parallel_writer; } void VM_HeapDumper::dump_stack_traces(AbstractDumpWriter* writer) { @@ -2584,6 +2638,17 @@ void VM_HeapDumper::dump_vthread(oop vt, AbstractDumpWriter* segment_writer) { ThreadDumper thread_dumper(ThreadDumper::ThreadType::UnmountedVirtual, nullptr, vt); thread_dumper.init_serial_nums(&_thread_serial_num, &_frame_serial_num); + // SapMachine 2026-05-06: If we don't do a parallel dump, we don't need the lock + // but have to end the current heap dump segment. + if (!is_parallel_dump()) { + segment_writer->finish_dump_segment(); + thread_dumper.dump_stack_traces(writer(), _klass_map); + thread_dumper.dump_thread_obj(segment_writer); + thread_dumper.dump_stack_refs(segment_writer); + + return; + } + // write HPROF_TRACE/HPROF_FRAME records to global writer _dumper_controller->lock_global_writer(); thread_dumper.dump_stack_traces(writer(), _klass_map); @@ -2734,7 +2799,9 @@ 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-05-06: Handle HeapDumpOverwrite and HeapDumpParallelism. + HeapDumper::dump_heap(false, true, tty, -1, HeapDumpOverwrite, HeapDumpParallelism == 0 ? + HeapDumper::default_num_of_dump_threads(): HeapDumpParallelism); } // Called by error reporting by a single Java thread outside of a JVM safepoint, @@ -2744,7 +2811,9 @@ 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-05-06: Handle HeapDumpOverwrite and HeapDumpParallelism. + HeapDumper::dump_heap(false, false, tty, -1, HeapDumpOverwrite, HeapDumpParallelism == 0 ? + HeapDumper::default_num_of_dump_threads() : HeapDumpParallelism); } // 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 00000000000..ab296e1a1cf --- /dev/null +++ b/test/hotspot/jtreg/serviceability/HeapDump/PartialArrayContentTest.java @@ -0,0 +1,246 @@ +/* + * 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.File; +import java.io.FileOutputStream; +import java.io.IOException; +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 { + public static int arraySize = 54321; + // 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(); + File firstSegment = new File(dumpFile + ".p0"); + fos = new FileOutputStream(firstSegment); + fos.close(); + createDump(dumpFile, false, + "-Xmx500M", + "-XX:+HeapDumpOnOutOfMemoryError", + "-XX:HeapDumpPath=" + dumpFile, + "-XX:+HeapDumpOverwrite", + "-XX:HeapDumpParallelism=1"); + verifyDump(dumpFile, false); + Asserts.assertTrue(firstSegment.exists(), "Segment file should not have been created (parallelism=1)."); + Asserts.assertEquals(firstSegment.length(), 0L, "Segment should not be modified."); + // 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 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; + } +}