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.hasJFR
46 * @requires vm.flagless
47 * @modules jdk.jfr/jdk.jfr.internal.test
48 * @library /test/lib /test/jdk
49 * @build jdk.jfr.event.oldobject.TestMetadataObject
50 * @run main/othervm -XX:TLABSize=2k -Xmx16m jdk.jfr.event.oldobject.TestMetadataRetention
51 */
52 public final class TestMetadataRetention {
53 private final static String TEST_PACKAGE = TestMetadataRetention.class.getPackage().getName();
54 private final static String TEST_CLASS_LOADER_NAME = "JFR TestClassLoader";
55 private final static String TEST_CLASS_NAME = TEST_PACKAGE + ".TestMetadataObject";
56 private final static String ALLOCATOR_THREAD_NAME = "TestAllocationThread";
57
58 public static ClassLoader testClassLoader;
59 public static Object leakObject;
60 public static Thread allocatorThread;
61 public static Class<?> testClass;
62
63 static class ChunkRotation extends Event {
64 }
65
66 public static void main(String[] args) throws Throwable {
67 WhiteBox.setWriteAllObjectSamples(true);
68 while (true) {
69 int failedAttempts = 0;
70 try (Recording recording = new Recording()) {
71 recording.enable(EventNames.OldObjectSample).withStackTrace();
72 recording.enable(EventNames.ClassUnload);
73 recording.start();
74
75 // Setup metadata on the Java heap (class, stack trace and thread)
76 testClassLoader = new TestClassLoader();
77 testClass = testClassLoader.loadClass(TEST_CLASS_NAME);
78 allocatorThread = new Thread(TestMetadataRetention::allocateLeak, ALLOCATOR_THREAD_NAME);
79 allocatorThread.start();
80 allocatorThread.join();
81
82 // Clear out metadata on the heap
83 testClassLoader = null;
84 testClass = null;
85 allocatorThread = null;
86
87 // System.gc() will trigger class unloading if -XX:+ExplicitGCInvokesConcurrent
88 // is NOT set. If this flag is set G1 will never unload classes on System.gc().
89 // As far as the vm.flagless guarantees no VM flags are set from the
90 // outside it should be enough with System.gc().
91 System.gc();
92
93 // Provoke a chunk rotation, which will flush out ordinary metadata.
94 provokeChunkRotation();
95 ChunkRotation cr = new ChunkRotation();
96 cr.commit();
97 recording.stop();
98
99 List<RecordedEvent> events = Events.fromRecording(recording);
100 RecordedEvent chunkRotation = findChunkRotationEvent(events);
101 try {
102 // Sanity check that class was unloaded
103 Events.hasEvent(events, EventNames.ClassUnload);
104 validateClassUnloadEvent(events);
105 // Validate that metadata for old object event has survived chunk rotation
106 Events.hasEvent(events, EventNames.OldObjectSample);
107 validateOldObjectEvent(events, chunkRotation.getStartTime());
108 } catch (Throwable t) {
109 t.printStackTrace();
110 System.out.println("Number of failed attempts " + ++failedAttempts);
111 continue;
112 }
113 break;
114 }
115 }
116 }
117
118 private static RecordedEvent findChunkRotationEvent(List<RecordedEvent> events) {
119 for (RecordedEvent e : events) {
120 if (e.getEventType().getName().equals(ChunkRotation.class.getName())) {
121 return e;
122 }
123 }
124 Asserts.fail("Could not find chunk rotation event");
125 return null; // Can't happen;
126 }
127
128 private static void allocateLeak() {
129 try {
130 leakObject = testClass.getMethod("startRecurse").invoke(null);
131 } catch (Exception e) {
132 System.out.println("Could not allocate memory leak!");
133 e.printStackTrace();
134 }
135 }
136
137 private static void provokeChunkRotation() {
138 try (Recording r = new Recording()) {
139 r.start();
140 r.stop();
141 }
142 }
143
144 private static void validateClassUnloadEvent(List<RecordedEvent> events) throws Throwable {
145 for (RecordedEvent event : events) {
146 if (event.getEventType().getName().equals(EventNames.ClassUnload)) {
147 RecordedClass unloadedClass = event.getValue("unloadedClass");
148 if (TEST_CLASS_NAME.equals(unloadedClass.getName())) {
149 RecordedClassLoader definingClassLoader = unloadedClass.getClassLoader();
150 Asserts.assertEquals(TEST_CLASS_LOADER_NAME, definingClassLoader.getName(), "Expected " + TEST_CLASS_LOADER_NAME + ", got " + definingClassLoader.getType().getName());
151 return;
152 }
153 }
154 }
155 }
156
157 private static void validateOldObjectEvent(List<RecordedEvent> events, Instant chunkRotation) throws Throwable {
158 for (RecordedEvent event : events) {
159 if (event.getEventType().getName().equals(EventNames.OldObjectSample)) {
160 // Only check event after the rotation
161 if (!event.getStartTime().isBefore(chunkRotation)) {
162 System.out.println(event);
163 RecordedThread rt = event.getThread();
164 if (rt.getJavaName().equals(ALLOCATOR_THREAD_NAME)) {
165 RecordedStackTrace s = event.getStackTrace();
166 assertStackTrace(s, "startRecurse");
167 assertStackTrace(s, "recurse");
168 return;
169 }
170 }
171 }
172 }
173
174 Asserts.fail("Did not find an old object event with thread " + ALLOCATOR_THREAD_NAME);
175 }
176
177 private static void assertStackTrace(RecordedStackTrace stacktrace, final String methodName) {
178 for (RecordedFrame f : stacktrace.getFrames()) {
179 if (f.getMethod().getName().equals(methodName)) {
180 return;
181 }
182 }
183 Asserts.fail("Could not find class " + methodName + " in stack trace " + stacktrace);
184 }
185 }