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 }