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 }