1 /* 2 * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package jdk.jfr.event.oldobject; 24 25 import java.time.Instant; 26 import java.util.List; 27 28 import jdk.jfr.Event; 29 import jdk.jfr.Recording; 30 import jdk.jfr.consumer.RecordedClass; 31 import jdk.jfr.consumer.RecordedClassLoader; 32 import jdk.jfr.consumer.RecordedEvent; 33 import jdk.jfr.consumer.RecordedFrame; 34 import jdk.jfr.consumer.RecordedStackTrace; 35 import jdk.jfr.consumer.RecordedThread; 36 import jdk.jfr.internal.test.WhiteBox; 37 import jdk.test.lib.Asserts; 38 import jdk.test.lib.jfr.EventNames; 39 import jdk.test.lib.jfr.Events; 40 import jdk.test.lib.jfr.TestClassLoader; 41 42 /** 43 * @test 44 * @summary The test verifies that an old object sample maintains references to "stale" metadata 45 * @requires vm.flagless 46 * @requires vm.hasJFR 47 * @requires !(vm.opt.final.UseCompactObjectHeaders == true | vm.opt.final.UseShenandoahGC == true) 48 * @modules jdk.jfr/jdk.jfr.internal.test 49 * @library /test/lib /test/jdk 50 * @build jdk.jfr.event.oldobject.TestMetadataObject 51 * @run main/othervm -XX:TLABSize=2k -Xmx16m jdk.jfr.event.oldobject.TestMetadataRetention 52 */ 53 public final class TestMetadataRetention { 54 private final static String TEST_PACKAGE = TestMetadataRetention.class.getPackage().getName(); 55 private final static String TEST_CLASS_LOADER_NAME = "JFR TestClassLoader"; 56 private final static String TEST_CLASS_NAME = TEST_PACKAGE + ".TestMetadataObject"; 57 private final static String ALLOCATOR_THREAD_NAME = "TestAllocationThread"; 58 59 public static ClassLoader testClassLoader; 60 public static Object leakObject; 61 public static Thread allocatorThread; 62 public static Class<?> testClass; 63 64 static class ChunkRotation extends Event { 65 } 66 67 public static void main(String[] args) throws Throwable { 68 WhiteBox.setWriteAllObjectSamples(true); 69 while (true) { 70 int failedAttempts = 0; 71 try (Recording recording = new Recording()) { 72 recording.enable(EventNames.OldObjectSample).withStackTrace(); 73 recording.enable(EventNames.ClassUnload); 74 recording.start(); 75 76 // Setup metadata on the Java heap (class, stack trace and thread) 77 testClassLoader = new TestClassLoader(); 78 testClass = testClassLoader.loadClass(TEST_CLASS_NAME); 79 allocatorThread = new Thread(TestMetadataRetention::allocateLeak, ALLOCATOR_THREAD_NAME); 80 allocatorThread.start(); 81 allocatorThread.join(); 82 83 // Clear out metadata on the heap 84 testClassLoader = null; 85 testClass = null; 86 allocatorThread = null; 87 88 // System.gc() will trigger class unloading if -XX:+ExplicitGCInvokesConcurrent 89 // is NOT set. If this flag is set G1 will never unload classes on System.gc(). 90 // As far as the vm.flagless guarantees no VM flags are set from the 91 // outside it should be enough with System.gc(). 92 System.gc(); 93 94 // Provoke a chunk rotation, which will flush out ordinary metadata. 95 provokeChunkRotation(); 96 ChunkRotation cr = new ChunkRotation(); 97 cr.commit(); 98 recording.stop(); 99 100 List<RecordedEvent> events = Events.fromRecording(recording); 101 RecordedEvent chunkRotation = findChunkRotationEvent(events); 102 try { 103 // Sanity check that class was unloaded 104 Events.hasEvent(events, EventNames.ClassUnload); 105 validateClassUnloadEvent(events); 106 // Validate that metadata for old object event has survived chunk rotation 107 Events.hasEvent(events, EventNames.OldObjectSample); 108 validateOldObjectEvent(events, chunkRotation.getStartTime()); 109 } catch (Throwable t) { 110 t.printStackTrace(); 111 System.out.println("Number of failed attempts " + ++failedAttempts); 112 continue; 113 } 114 break; 115 } 116 } 117 } 118 119 private static RecordedEvent findChunkRotationEvent(List<RecordedEvent> events) { 120 for (RecordedEvent e : events) { 121 if (e.getEventType().getName().equals(ChunkRotation.class.getName())) { 122 return e; 123 } 124 } 125 Asserts.fail("Could not find chunk rotation event"); 126 return null; // Can't happen; 127 } 128 129 private static void allocateLeak() { 130 try { 131 leakObject = testClass.getMethod("startRecurse").invoke(null); 132 } catch (Exception e) { 133 System.out.println("Could not allocate memory leak!"); 134 e.printStackTrace(); 135 } 136 } 137 138 private static void provokeChunkRotation() { 139 try (Recording r = new Recording()) { 140 r.start(); 141 r.stop(); 142 } 143 } 144 145 private static void validateClassUnloadEvent(List<RecordedEvent> events) throws Throwable { 146 for (RecordedEvent event : events) { 147 if (event.getEventType().getName().equals(EventNames.ClassUnload)) { 148 RecordedClass unloadedClass = event.getValue("unloadedClass"); 149 if (TEST_CLASS_NAME.equals(unloadedClass.getName())) { 150 RecordedClassLoader definingClassLoader = unloadedClass.getClassLoader(); 151 Asserts.assertEquals(TEST_CLASS_LOADER_NAME, definingClassLoader.getName(), "Expected " + TEST_CLASS_LOADER_NAME + ", got " + definingClassLoader.getType().getName()); 152 return; 153 } 154 } 155 } 156 } 157 158 private static void validateOldObjectEvent(List<RecordedEvent> events, Instant chunkRotation) throws Throwable { 159 for (RecordedEvent event : events) { 160 if (event.getEventType().getName().equals(EventNames.OldObjectSample)) { 161 // Only check event after the rotation 162 if (!event.getStartTime().isBefore(chunkRotation)) { 163 System.out.println(event); 164 RecordedThread rt = event.getThread(); 165 if (rt.getJavaName().equals(ALLOCATOR_THREAD_NAME)) { 166 RecordedStackTrace s = event.getStackTrace(); 167 assertStackTrace(s, "startRecurse"); 168 assertStackTrace(s, "recurse"); 169 return; 170 } 171 } 172 } 173 } 174 175 Asserts.fail("Did not find an old object event with thread " + ALLOCATOR_THREAD_NAME); 176 } 177 178 private static void assertStackTrace(RecordedStackTrace stacktrace, final String methodName) { 179 for (RecordedFrame f : stacktrace.getFrames()) { 180 if (f.getMethod().getName().equals(methodName)) { 181 return; 182 } 183 } 184 Asserts.fail("Could not find class " + methodName + " in stack trace " + stacktrace); 185 } 186 } --- EOF ---