From 4f123fbe20b2b1c6ada1890c9c33bacaf5033822 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sun, 5 Jul 2026 12:21:27 +0200 Subject: [PATCH 1/2] Fix #6357: unresolved sticky class GC roots Assisted-by: OpenAI GPT-5 Codex --- .../lib/profiler/heap/HprofGCRoots.java | 5 +++- .../lib/profiler/heap/HeapSegmentTest.java | 24 +++++++++++++++++++ .../netbeans/lib/profiler/heap/HeapUtils.java | 5 ++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java b/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java index 9da02c8bec32..bb58b5d3626d 100644 --- a/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java +++ b/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java @@ -93,7 +93,10 @@ GCRoot getGCRoot(Long instanceId) { heap.getGCRoots(); roots = new HashMap<>(); for (GCRoot r : getGCRoots()) { - roots.put(r.getInstance().getInstanceId(), r); + Instance instance = r.getInstance(); + if (instance != null) { + roots.put(instance.getInstanceId(), r); + } } gcRoots = roots; } else { diff --git a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java index 87c01062424f..a9353f5e1c0c 100644 --- a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java +++ b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java @@ -31,6 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import org.junit.Test; import org.netbeans.lib.profiler.heap.HeapUtils.HprofGenerator; @@ -45,6 +46,29 @@ public void singleObjectMultipleSegments() throws IOException { singleObject(true); } + @Test + public void unresolvedStickyClassRootDoesNotBreakGCRootLookup() throws IOException { + File mydump = File.createTempFile("mydump", ".hprof"); + try (HprofGenerator gen = new HprofGenerator(new FileOutputStream(mydump))) { + gen.writeHeapSegment(new HprofGenerator.Generator() { + @Override + public void generate(HprofGenerator.HeapSegment seg) throws IOException { + seg.newClass("com.oracle.svm.core.heap.heapImpl.DiscoverableReference") + .addField("rawReferent", Object.class) + .dumpClass(); + HprofGenerator.ClassInstance clazz = seg.newClass("text.HelloWorld").dumpClass(); + seg.dumpStickyClassRoot(clazz); + seg.dumpInstance(clazz); + } + }, true); + } + + Heap heap = HeapFactory.createHeap(mydump); + assertEquals("One unresolved sticky class root", 1, heap.getGCRoots().size()); + Instance instance = (Instance) heap.getJavaClassByName("text.HelloWorld").getInstances().iterator().next(); + assertNull("Non-root instance", heap.getGCRoot(instance)); + } + private static void singleObject(boolean flush) throws IOException { File mydump = File.createTempFile("mydump", ".hprof"); Heap heap = generateSampleDump(mydump, flush); diff --git a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java index 115c99c63832..4ac8e61b9edb 100644 --- a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java +++ b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java @@ -170,6 +170,11 @@ public int dumpPrimitive(Object obj) throws IOException { return instanceId; } + public void dumpStickyClassRoot(ClassInstance clazz) throws IOException { + heap.writeByte(0x05); + heap.writeInt(clazz.id); + } + public final class ThreadBuilder { private String groupName; From 66beb5d8cbe4f98b4fbc44eebeffe7cbfe140d96 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 10 Jul 2026 22:25:49 +0200 Subject: [PATCH 2/2] Fix #6357: retain sticky class GC roots by ID Assisted-by: OpenAI GPT-5 Codex --- .../lib/profiler/heap/HprofGCRoots.java | 23 ++++----- .../lib/profiler/heap/HeapSegmentTest.java | 51 ++++++++++++++++--- .../netbeans/lib/profiler/heap/HeapUtils.java | 20 +++++++- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java b/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java index bb58b5d3626d..dba19d9cc68a 100644 --- a/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java +++ b/profiler/lib.profiler/src/org/netbeans/lib/profiler/heap/HprofGCRoots.java @@ -39,16 +39,16 @@ class HprofGCRoots { private final Object lastThreadObjGCLock = new Object(); private Map gcRoots; private final Object gcRootLock = new Object(); - private List gcRootsList; + private List gcRootsList; HprofGCRoots(HprofHeap h) { heap = h; } - Collection getGCRoots() { + Collection getGCRoots() { synchronized (gcRootLock) { if (gcRoots == null) { - List rootList = new ArrayList<>(); + List rootList = new ArrayList<>(); computeGCRootsFor(heap.getHeapTagBound(HprofHeap.ROOT_UNKNOWN), rootList); computeGCRootsFor(heap.getHeapTagBound(HprofHeap.ROOT_JNI_GLOBAL), rootList); computeGCRootsFor(heap.getHeapTagBound(HprofHeap.ROOT_JNI_LOCAL), rootList); @@ -67,10 +67,8 @@ Collection getGCRoots() { computeGCRootsFor(heap.getHeapTagBound(HprofHeap.ROOT_VM_INTERNAL), rootList); computeGCRootsFor(heap.getHeapTagBound(HprofHeap.ROOT_JNI_MONITOR), rootList); - rootList.sort(new Comparator() { - public int compare(Object o1, Object o2) { - HprofGCRoot r1 = (HprofGCRoot) o1; - HprofGCRoot r2 = (HprofGCRoot) o2; + rootList.sort(new Comparator() { + public int compare(HprofGCRoot r1, HprofGCRoot r2) { int kind = r1.getKind().compareTo(r2.getKind()); if (kind != 0) { @@ -92,11 +90,10 @@ GCRoot getGCRoot(Long instanceId) { if (gcRoots == null) { heap.getGCRoots(); roots = new HashMap<>(); - for (GCRoot r : getGCRoots()) { - Instance instance = r.getInstance(); - if (instance != null) { - roots.put(instance.getInstanceId(), r); - } + for (HprofGCRoot root : getGCRoots()) { + // A GC root is identified by its HPROF ID. Some valid root + // records do not have a corresponding heap Instance. + roots.put(root.getInstanceId(), root); } gcRoots = roots; } else { @@ -129,7 +126,7 @@ ThreadObjectGCRoot getThreadGCRoot(int threadSerialNumber) { return map.get(threadSerialNumber); } - private void computeGCRootsFor(TagBounds tagBounds, Collection roots) { + private void computeGCRootsFor(TagBounds tagBounds, Collection roots) { if (tagBounds != null) { int rootTag = tagBounds.tag; long[] offset = new long[]{tagBounds.startOffset}; diff --git a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java index a9353f5e1c0c..28a96062843c 100644 --- a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java +++ b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapSegmentTest.java @@ -32,6 +32,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; import org.netbeans.lib.profiler.heap.HeapUtils.HprofGenerator; @@ -47,26 +48,64 @@ public void singleObjectMultipleSegments() throws IOException { } @Test - public void unresolvedStickyClassRootDoesNotBreakGCRootLookup() throws IOException { + public void stickyClassRootKeepsStaticReferencesReachable() throws IOException { File mydump = File.createTempFile("mydump", ".hprof"); + final int[] targetId = new int[1]; try (HprofGenerator gen = new HprofGenerator(new FileOutputStream(mydump))) { gen.writeHeapSegment(new HprofGenerator.Generator() { @Override public void generate(HprofGenerator.HeapSegment seg) throws IOException { + seg.newClass("java.lang.Class").dumpClass(); seg.newClass("com.oracle.svm.core.heap.heapImpl.DiscoverableReference") .addField("rawReferent", Object.class) .dumpClass(); - HprofGenerator.ClassInstance clazz = seg.newClass("text.HelloWorld").dumpClass(); - seg.dumpStickyClassRoot(clazz); - seg.dumpInstance(clazz); + HprofGenerator.ClassInstance targetClass = seg.newClass("text.Target").dumpClass(); + targetId[0] = seg.dumpInstance(targetClass); + HprofGenerator.ClassInstance rootClass = seg.newClass("text.Root") + .addStaticObjectField("target", targetId[0]) + .dumpClass(); + seg.dumpStickyClassRoot(rootClass); + } + }, true); + } + + Heap heap = HeapFactory.createHeap(mydump); + assertEquals("One sticky class root", 1, heap.getGCRoots().size()); + GCRoot root = (GCRoot) heap.getGCRoots().iterator().next(); + Instance rootInstance = root.getInstance(); + assertTrue("Class object instance", rootInstance instanceof ClassDumpInstance); + assertEquals("Root found by class object ID", root, heap.getGCRoot(rootInstance)); + + Instance target = heap.getInstanceByID(targetId[0]); + assertNotNull("Static field target", target); + heap.getBiggestObjectsByRetainedSize(1); + assertEquals("Target reached from sticky class", rootInstance, target.getNearestGCRootPointer()); + assertTrue("Sticky class retains its static target", rootInstance.getRetainedSize() > rootInstance.getSize()); + } + + @Test + public void unresolvedStickyClassRootDoesNotBreakRetainedSize() throws IOException { + File mydump = File.createTempFile("mydump", ".hprof"); + try (HprofGenerator gen = new HprofGenerator(new FileOutputStream(mydump))) { + gen.writeHeapSegment(new HprofGenerator.Generator() { + @Override + public void generate(HprofGenerator.HeapSegment seg) throws IOException { + seg.newClass("java.lang.Class").dumpClass(); + seg.newClass("com.oracle.svm.core.heap.heapImpl.DiscoverableReference") + .addField("rawReferent", Object.class) + .dumpClass(); + HprofGenerator.ClassInstance ordinaryClass = seg.newClass("text.Ordinary").dumpClass(); + seg.dumpInstance(ordinaryClass); + seg.dumpStickyClassRoot(Integer.MAX_VALUE); } }, true); } Heap heap = HeapFactory.createHeap(mydump); assertEquals("One unresolved sticky class root", 1, heap.getGCRoots().size()); - Instance instance = (Instance) heap.getJavaClassByName("text.HelloWorld").getInstances().iterator().next(); - assertNull("Non-root instance", heap.getGCRoot(instance)); + GCRoot root = (GCRoot) heap.getGCRoots().iterator().next(); + assertNull("No heap object for unresolved root", root.getInstance()); + assertNotNull("Retained-size computation completes", heap.getBiggestObjectsByRetainedSize(1)); } private static void singleObject(boolean flush) throws IOException { diff --git a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java index 4ac8e61b9edb..047cd78ed79e 100644 --- a/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java +++ b/profiler/lib.profiler/test/unit/src/org/netbeans/lib/profiler/heap/HeapUtils.java @@ -171,8 +171,12 @@ public int dumpPrimitive(Object obj) throws IOException { } public void dumpStickyClassRoot(ClassInstance clazz) throws IOException { + dumpStickyClassRoot(clazz.id); + } + + public void dumpStickyClassRoot(int classId) throws IOException { heap.writeByte(0x05); - heap.writeInt(clazz.id); + heap.writeInt(classId); } public final class ThreadBuilder { @@ -240,6 +244,7 @@ public final class ClassBuilder { private final int classId; private TreeMap> fieldNamesAndTypes = new TreeMap<>(); + private TreeMap staticObjectFields = new TreeMap<>(); private ClassBuilder(int id) { this.classId = id; @@ -250,6 +255,11 @@ public ClassBuilder addField(String name, Class type) { return this; } + public ClassBuilder addStaticObjectField(String name, int instanceId) { + staticObjectFields.put(name, instanceId); + return this; + } + public ClassInstance dumpClass() throws IOException { heap.writeByte(0x20); heap.writeInt(classId); // class ID @@ -262,7 +272,12 @@ public ClassInstance dumpClass() throws IOException { heap.writeInt(0); // reserved 2 heap.writeInt(0); // instance size heap.writeShort(0); // # of constant pool entries - heap.writeShort(0); // # of static fields + heap.writeShort(staticObjectFields.size()); // # of static fields + for (Map.Entry entry : staticObjectFields.entrySet()) { + heap.writeInt(writeString(entry.getKey())); + heap.writeByte(0x02); // object + heap.writeInt(entry.getValue()); + } heap.writeShort(fieldNamesAndTypes.size()); // # of instance fields int fieldBytes = 0; for (Map.Entry> entry : fieldNamesAndTypes.entrySet()) { @@ -304,6 +319,7 @@ public ClassInstance dumpClass() throws IOException { } ClassInstance inst = new ClassInstance(classId, fieldNamesAndTypes, fieldBytes); fieldNamesAndTypes = new TreeMap<>(); + staticObjectFields = new TreeMap<>(); return inst; } }