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 }