Date: Fri, 9 Jun 2023 12:13:26 +0800
Subject: [PATCH 47/59] 8025692: Log what methods are touched at run-time

Bug url: https://bugs.openjdk.org/browse/JDK-8025692
---
 .../vm/templateInterpreter_aarch64.cpp        |   4 +-
 hotspot/src/cpu/ppc/vm/interp_masm_ppc_64.cpp |   2 +-
 .../cpu/ppc/vm/templateInterpreter_ppc.cpp    |   4 +-
 .../src/cpu/sparc/vm/interp_masm_sparc.cpp    |   2 +-
 .../sparc/vm/templateInterpreter_sparc.cpp    |   4 +-
 .../cpu/x86/vm/templateInterpreter_x86_32.cpp |   4 +-
 .../cpu/x86/vm/templateInterpreter_x86_64.cpp |   4 +-
 hotspot/src/share/vm/ci/ciMethod.cpp          |   3 +
 hotspot/src/share/vm/oops/method.cpp          |  84 +++++++++++
 hotspot/src/share/vm/oops/method.hpp          |   2 +
 hotspot/src/share/vm/runtime/globals.hpp      |   6 +
 hotspot/src/share/vm/runtime/java.cpp         |   8 ++
 hotspot/src/share/vm/runtime/mutexLocker.cpp  |   2 +
 hotspot/src/share/vm/runtime/mutexLocker.hpp  |   1 +
 .../src/share/vm/runtime/vm_operations.hpp    |   1 +
 .../share/vm/services/diagnosticCommand.cpp   |  33 +++++
 .../share/vm/services/diagnosticCommand.hpp   |  17 +++
 .../CommandLine/PrintTouchedMethods.java      | 134 ++++++++++++++++++
 .../CommandLine/TestLogTouchedMethods.java    |  35 +++++
 19 files changed, 338 insertions(+), 12 deletions(-)
 create mode 100644 hotspot/test/runtime/CommandLine/PrintTouchedMethods.java
 create mode 100644 hotspot/test/runtime/CommandLine/TestLogTouchedMethods.java

diff --git a/hotspot/src/cpu/aarch64/vm/templateInterpreter_aarch64.cpp b/hotspot/src/cpu/aarch64/vm/templateInterpreter_aarch64.cpp
index a5a91e5f3..28b84cb51 100644
--- a/hotspot/src/cpu/aarch64/vm/templateInterpreter_aarch64.cpp
+++ b/hotspot/src/cpu/aarch64/vm/templateInterpreter_aarch64.cpp
@@ -1135,7 +1135,7 @@ void InterpreterGenerator::bang_stack_shadow_pages(bool native_call) {
 // native method than the typical interpreter frame setup.
 address InterpreterGenerator::generate_native_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // r1: Method*
   // rscratch1: sender sp
@@ -1591,7 +1591,7 @@ address InterpreterGenerator::generate_native_entry(bool synchronized) {
 //
 address InterpreterGenerator::generate_normal_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // rscratch1: sender sp
   address entry_point = __ pc();
diff --git a/hotspot/src/cpu/ppc/vm/interp_masm_ppc_64.cpp b/hotspot/src/cpu/ppc/vm/interp_masm_ppc_64.cpp
index 463f9da92..594905ef7 100644
--- a/hotspot/src/cpu/ppc/vm/interp_masm_ppc_64.cpp
+++ b/hotspot/src/cpu/ppc/vm/interp_masm_ppc_64.cpp
@@ -2226,7 +2226,7 @@ void InterpreterMacroAssembler::get_method_counters(Register method,
 }
 
 void InterpreterMacroAssembler::increment_invocation_counter(Register Rcounters, Register iv_be_count, Register Rtmp_r0) {
-  assert(UseCompiler, "incrementing must be useful");
+  assert(UseCompiler || LogTouchedMethods, "incrementing must be useful");
   Register invocation_count = iv_be_count;
   Register backedge_count   = Rtmp_r0;
   int delta = InvocationCounter::count_increment;
diff --git a/hotspot/src/cpu/ppc/vm/templateInterpreter_ppc.cpp b/hotspot/src/cpu/ppc/vm/templateInterpreter_ppc.cpp
index 0fb934166..4e2cec39b 100644
--- a/hotspot/src/cpu/ppc/vm/templateInterpreter_ppc.cpp
+++ b/hotspot/src/cpu/ppc/vm/templateInterpreter_ppc.cpp
@@ -726,7 +726,7 @@ address TemplateInterpreterGenerator::generate_native_entry(bool synchronized) {
 
   address entry = __ pc();
 
-  const bool inc_counter = UseCompiler || CountCompiledCalls;
+  const bool inc_counter = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // -----------------------------------------------------------------------------
   // Allocate a new frame that represents the native callee (i2n frame).
@@ -1176,7 +1176,7 @@ address TemplateInterpreterGenerator::generate_native_entry(bool synchronized) {
 // Generic interpreted method entry to (asm) interpreter.
 //
 address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
-  bool inc_counter = UseCompiler || CountCompiledCalls;
+  bool inc_counter = UseCompiler || CountCompiledCalls || LogTouchedMethods;
   address entry = __ pc();
   // Generate the code to allocate the interpreter stack frame.
   Register Rsize_of_parameters = R4_ARG2, // Written by generate_fixed_frame.
diff --git a/hotspot/src/cpu/sparc/vm/interp_masm_sparc.cpp b/hotspot/src/cpu/sparc/vm/interp_masm_sparc.cpp
index 66bcfcaf1..83d54d731 100644
--- a/hotspot/src/cpu/sparc/vm/interp_masm_sparc.cpp
+++ b/hotspot/src/cpu/sparc/vm/interp_masm_sparc.cpp
@@ -2362,7 +2362,7 @@ void InterpreterMacroAssembler::get_method_counters(Register method,
 }
 
 void InterpreterMacroAssembler::increment_invocation_counter( Register Rcounters, Register Rtmp, Register Rtmp2 ) {
-  assert(UseCompiler, "incrementing must be useful");
+  assert(UseCompiler || LogTouchedMethods, "incrementing must be useful");
   assert_different_registers(Rcounters, Rtmp, Rtmp2);
 
   Address inv_counter(Rcounters, MethodCounters::invocation_counter_offset() +
diff --git a/hotspot/src/cpu/sparc/vm/templateInterpreter_sparc.cpp b/hotspot/src/cpu/sparc/vm/templateInterpreter_sparc.cpp
index 540f9b287..9526866bd 100644
--- a/hotspot/src/cpu/sparc/vm/templateInterpreter_sparc.cpp
+++ b/hotspot/src/cpu/sparc/vm/templateInterpreter_sparc.cpp
@@ -823,7 +823,7 @@ address InterpreterGenerator::generate_native_entry(bool synchronized) {
   // the following temporary registers are used during frame creation
   const Register Gtmp1 = G3_scratch ;
   const Register Gtmp2 = G1_scratch;
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // make sure registers are different!
   assert_different_registers(G2_thread, G5_method, Gargs, Gtmp1, Gtmp2);
@@ -1261,7 +1261,7 @@ address InterpreterGenerator::generate_native_entry(bool synchronized) {
 address InterpreterGenerator::generate_normal_entry(bool synchronized) {
   address entry = __ pc();
 
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // the following temporary registers are used during frame creation
   const Register Gtmp1 = G3_scratch ;
diff --git a/hotspot/src/cpu/x86/vm/templateInterpreter_x86_32.cpp b/hotspot/src/cpu/x86/vm/templateInterpreter_x86_32.cpp
index b7c515031..662d82222 100644
--- a/hotspot/src/cpu/x86/vm/templateInterpreter_x86_32.cpp
+++ b/hotspot/src/cpu/x86/vm/templateInterpreter_x86_32.cpp
@@ -990,7 +990,7 @@ address InterpreterGenerator::generate_CRC32_updateBytes_entry(AbstractInterpret
 
 address InterpreterGenerator::generate_native_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // rbx,: Method*
   // rsi: sender sp
@@ -1405,7 +1405,7 @@ address InterpreterGenerator::generate_native_entry(bool synchronized) {
 //
 address InterpreterGenerator::generate_normal_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // rbx,: Method*
   // rsi: sender sp
diff --git a/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp b/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp
index 209a3f676..0f3bfc979 100644
--- a/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp
+++ b/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp
@@ -962,7 +962,7 @@ address InterpreterGenerator::generate_CRC32_updateBytes_entry(AbstractInterpret
 // native method than the typical interpreter frame setup.
 address InterpreterGenerator::generate_native_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // rbx: Method*
   // r13: sender sp
@@ -1409,7 +1409,7 @@ address InterpreterGenerator::generate_native_entry(bool synchronized) {
 //
 address InterpreterGenerator::generate_normal_entry(bool synchronized) {
   // determine code generation flags
-  bool inc_counter  = UseCompiler || CountCompiledCalls;
+  bool inc_counter  = UseCompiler || CountCompiledCalls || LogTouchedMethods;
 
   // ebx: Method*
   // r13: sender sp
diff --git a/hotspot/src/share/vm/ci/ciMethod.cpp b/hotspot/src/share/vm/ci/ciMethod.cpp
index 22fe6c8f1..50fafc4f8 100644
--- a/hotspot/src/share/vm/ci/ciMethod.cpp
+++ b/hotspot/src/share/vm/ci/ciMethod.cpp
@@ -74,6 +74,9 @@ ciMethod::ciMethod(methodHandle h_m, ciInstanceKlass* holder) :
 {
   assert(h_m() != NULL, "no null method");
 
+  if (LogTouchedMethods) {
+    h_m()->log_touched(Thread::current());
+  }
   // These fields are always filled in in loaded methods.
   _flags = ciFlags(h_m()->access_flags());
 
diff --git a/hotspot/src/share/vm/oops/method.cpp b/hotspot/src/share/vm/oops/method.cpp
index 305348bd0..406cd485e 100644
--- a/hotspot/src/share/vm/oops/method.cpp
+++ b/hotspot/src/share/vm/oops/method.cpp
@@ -408,6 +408,11 @@ MethodCounters* Method::build_method_counters(Method* m, TRAPS) {
   if (!mh->init_method_counters(counters)) {
     MetadataFactory::free_metadata(loader_data, counters);
   }
+
+  if (LogTouchedMethods) {
+    mh->log_touched(CHECK_NULL);
+  }
+
   return mh->method_counters();
 }
 
@@ -2088,6 +2093,85 @@ void Method::collect_statistics(KlassSizeStats *sz) const {
 }
 #endif // INCLUDE_SERVICES
 
+// LogTouchedMethods and PrintTouchedMethods
+
+// TouchedMethodRecord -- we can't use a HashtableEntry<Method*> because
+// the Method may be garbage collected. Let's roll our own hash table.
+class TouchedMethodRecord : CHeapObj<mtTracing> {
+public:
+  // It's OK to store Symbols here because they will NOT be GC'ed if
+  // LogTouchedMethods is enabled.
+  TouchedMethodRecord* _next;
+  Symbol* _class_name;
+  Symbol* _method_name;
+  Symbol* _method_signature;
+};
+
+static const int TOUCHED_METHOD_TABLE_SIZE = 20011;
+static TouchedMethodRecord** _touched_method_table = NULL;
+
+void Method::log_touched(TRAPS) {
+
+  const int table_size = TOUCHED_METHOD_TABLE_SIZE;
+  Symbol* my_class = klass_name();
+  Symbol* my_name  = name();
+  Symbol* my_sig   = signature();
+
+  unsigned int hash = my_class->identity_hash() +
+                      my_name->identity_hash() +
+                      my_sig->identity_hash();
+  juint index = juint(hash) % table_size;
+
+  MutexLocker ml(TouchedMethodLog_lock, THREAD);
+  if (_touched_method_table == NULL) {
+    _touched_method_table = NEW_C_HEAP_ARRAY2(TouchedMethodRecord*, table_size,
+                                              mtTracing, CURRENT_PC);
+    memset(_touched_method_table, 0, sizeof(TouchedMethodRecord*) * table_size);
+  }
+
+  TouchedMethodRecord* ptr = _touched_method_table[index];
+  while (ptr) {
+    if (ptr->_class_name       == my_class &&
+        ptr->_method_name      == my_name &&
+        ptr->_method_signature == my_sig) {
+      return;
+    }
+    if (ptr->_next == NULL) break;
+    ptr = ptr->_next;
+  }
+  TouchedMethodRecord* nptr = NEW_C_HEAP_OBJ(TouchedMethodRecord, mtTracing);
+  my_class->increment_refcount();
+  my_name->increment_refcount();
+  my_sig->increment_refcount();
+  nptr->_class_name         = my_class;
+  nptr->_method_name        = my_name;
+  nptr->_method_signature   = my_sig;
+  nptr->_next               = NULL;
+
+  if (ptr == NULL) {
+    // first
+    _touched_method_table[index] = nptr;
+  } else {
+    ptr->_next = nptr;
+  }
+}
+
+void Method::print_touched_methods(outputStream* out) {
+  MutexLockerEx ml(Thread::current()->is_VM_thread() ? NULL : TouchedMethodLog_lock);
+  out->print_cr("# Method::print_touched_methods version 1");
+  if (_touched_method_table) {
+    for (int i = 0; i < TOUCHED_METHOD_TABLE_SIZE; i++) {
+      TouchedMethodRecord* ptr = _touched_method_table[i];
+      while(ptr) {
+        ptr->_class_name->print_symbol_on(out);       out->print(".");
+        ptr->_method_name->print_symbol_on(out);      out->print(":");
+        ptr->_method_signature->print_symbol_on(out); out->cr();
+        ptr = ptr->_next;
+      }
+    }
+  }
+}
+
 // Verification
 
 void Method::verify_on(outputStream* st) {
diff --git a/hotspot/src/share/vm/oops/method.hpp b/hotspot/src/share/vm/oops/method.hpp
index ec93f2fb4..ee74d959d 100644
--- a/hotspot/src/share/vm/oops/method.hpp
+++ b/hotspot/src/share/vm/oops/method.hpp
@@ -650,6 +650,8 @@ class Method : public Metadata {
 #if INCLUDE_SERVICES
   void collect_statistics(KlassSizeStats *sz) const;
 #endif
+  void log_touched(TRAPS);
+  static void print_touched_methods(outputStream* out);
 
   // interpreter support
   static ByteSize const_offset()                 { return byte_offset_of(Method, _constMethod       ); }
diff --git a/hotspot/src/share/vm/runtime/globals.hpp b/hotspot/src/share/vm/runtime/globals.hpp
index 210e6bd67..11f713013 100644
--- a/hotspot/src/share/vm/runtime/globals.hpp
+++ b/hotspot/src/share/vm/runtime/globals.hpp
@@ -2694,6 +2694,12 @@ class CommandLineFlags {
   develop(bool, EagerInitialization, false,                                 \
           "Eagerly initialize classes if possible")                         \
                                                                             \
+  diagnostic(bool, LogTouchedMethods, false,                                \
+          "Log methods which have been ever touched in runtime")            \
+                                                                            \
+  diagnostic(bool, PrintTouchedMethodsAtExit, false,                        \
+          "Print all methods that have been ever touched in runtime")       \
+                                                                            \
   develop(bool, TraceMethodReplacement, false,                              \
           "Print when methods are replaced do to recompilation")            \
                                                                             \
diff --git a/hotspot/src/share/vm/runtime/java.cpp b/hotspot/src/share/vm/runtime/java.cpp
index c72a5a766..5a628b73e 100644
--- a/hotspot/src/share/vm/runtime/java.cpp
+++ b/hotspot/src/share/vm/runtime/java.cpp
@@ -360,6 +360,10 @@ void print_statistics() {
     SystemDictionary::print();
   }
 
+  if (LogTouchedMethods && PrintTouchedMethodsAtExit) {
+    Method::print_touched_methods(tty);
+  }
+
   if (PrintBiasedLockingStatistics) {
     BiasedLocking::print_counters();
   }
@@ -408,6 +412,10 @@ void print_statistics() {
   if (PrintNMTStatistics) {
     MemTracker::final_report(tty);
   }
+
+  if (LogTouchedMethods && PrintTouchedMethodsAtExit) {
+    Method::print_touched_methods(tty);
+  }
 }
 
 #endif
diff --git a/hotspot/src/share/vm/runtime/mutexLocker.cpp b/hotspot/src/share/vm/runtime/mutexLocker.cpp
index a1c61f864..0b4a98cb7 100644
--- a/hotspot/src/share/vm/runtime/mutexLocker.cpp
+++ b/hotspot/src/share/vm/runtime/mutexLocker.cpp
@@ -63,6 +63,7 @@ Monitor* StringDedupQueue_lock        = NULL;
 Mutex*   StringDedupTable_lock        = NULL;
 Mutex*   CodeCache_lock               = NULL;
 Mutex*   MethodData_lock              = NULL;
+Mutex*   TouchedMethodLog_lock        = NULL;
 Mutex*   RetData_lock                 = NULL;
 Monitor* VMOperationQueue_lock        = NULL;
 Monitor* VMOperationRequest_lock      = NULL;
@@ -282,6 +283,7 @@ void mutex_init() {
 
   def(Compile_lock                 , Mutex  , nonleaf+3,   true );
   def(MethodData_lock              , Mutex  , nonleaf+3,   false);
+  def(TouchedMethodLog_lock        , Mutex  , nonleaf+3,   false);
 
   def(MethodCompileQueue_lock      , Monitor, nonleaf+4,   true );
   def(Debug2_lock                  , Mutex  , nonleaf+4,   true );
diff --git a/hotspot/src/share/vm/runtime/mutexLocker.hpp b/hotspot/src/share/vm/runtime/mutexLocker.hpp
index f28058b0e..0d6de9ea0 100644
--- a/hotspot/src/share/vm/runtime/mutexLocker.hpp
+++ b/hotspot/src/share/vm/runtime/mutexLocker.hpp
@@ -71,6 +71,7 @@ extern Monitor* StringDedupQueue_lock;           // a lock on the string dedupli
 extern Mutex*   StringDedupTable_lock;           // a lock on the string deduplication table
 extern Mutex*   CodeCache_lock;                  // a lock on the CodeCache, rank is special, use MutexLockerEx
 extern Mutex*   MethodData_lock;                 // a lock on installation of method data
+extern Mutex*   TouchedMethodLog_lock;           // a lock on allocation of LogExecutedMethods info
 extern Mutex*   RetData_lock;                    // a lock on installation of RetData inside method data
 extern Mutex*   DerivedPointerTableGC_lock;      // a lock to protect the derived pointer table
 extern Monitor* VMOperationQueue_lock;           // a lock on queue of vm_operations waiting to execute
diff --git a/hotspot/src/share/vm/runtime/vm_operations.hpp b/hotspot/src/share/vm/runtime/vm_operations.hpp
index 19463b137..f2071a9d6 100644
--- a/hotspot/src/share/vm/runtime/vm_operations.hpp
+++ b/hotspot/src/share/vm/runtime/vm_operations.hpp
@@ -99,6 +99,7 @@
   template(WhiteBoxOperation)                     \
   template(ClassLoaderStatsOperation)             \
   template(ClassLoaderHierarchyOperation)         \
+  template(DumpTouchedMethods)                    \
   template(JFROldObject)                          \
   template(PrintClasses)                          \
   template(PrintMetadata)                         \
diff --git a/hotspot/src/share/vm/services/diagnosticCommand.cpp b/hotspot/src/share/vm/services/diagnosticCommand.cpp
index e37ba3cc8..50050a169 100644
--- a/hotspot/src/share/vm/services/diagnosticCommand.cpp
+++ b/hotspot/src/share/vm/services/diagnosticCommand.cpp
@@ -76,6 +76,7 @@ void DCmdRegistrant::register_dcmds(){
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CompileQueueDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeListDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeCacheDCmd>(full_export, true, false));
+  DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<TouchedMethodsDCmd>(full_export, true, false));
 #ifdef LINUX
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<TrimCLibcHeapDCmd>(full_export, true, false));
 #endif // LINUX
@@ -569,6 +570,38 @@ int ThreadDumpDCmd::num_arguments() {
   }
 }
 
+class VM_DumpTouchedMethods : public VM_Operation {
+private:
+  outputStream* _out;
+public:
+  VM_DumpTouchedMethods(outputStream* out) {
+    _out = out;
+  }
+
+  virtual VMOp_Type type() const { return VMOp_DumpTouchedMethods; }
+
+  virtual void doit() {
+    Method::print_touched_methods(_out);
+  }
+};
+
+TouchedMethodsDCmd::TouchedMethodsDCmd(outputStream* output, bool heap) :
+                                       DCmdWithParser(output, heap)
+{}
+
+void TouchedMethodsDCmd::execute(DCmdSource source, TRAPS) {
+  if (!UnlockDiagnosticVMOptions) {
+    output()->print_cr("VM.touched_methods command requires -XX:+UnlockDiagnosticVMOptions");
+    return;
+  }
+  VM_DumpTouchedMethods dumper(output());
+  VMThread::execute(&dumper);
+}
+
+int TouchedMethodsDCmd::num_arguments() {
+  return 0;
+}
+
 // Enhanced JMX Agent support
 
 JMXStartRemoteDCmd::JMXStartRemoteDCmd(outputStream *output, bool heap_allocated) :
diff --git a/hotspot/src/share/vm/services/diagnosticCommand.hpp b/hotspot/src/share/vm/services/diagnosticCommand.hpp
index 65c04571b..3733fa7f7 100644
--- a/hotspot/src/share/vm/services/diagnosticCommand.hpp
+++ b/hotspot/src/share/vm/services/diagnosticCommand.hpp
@@ -36,6 +36,7 @@
 #include "services/diagnosticFramework.hpp"
 #include "services/diagnosticCommand_ext.hpp"
 #include "utilities/macros.hpp"
+#include "oops/method.hpp"
 
 class HelpDCmd : public DCmdWithParser {
 protected:
@@ -358,6 +359,22 @@ public:
   virtual void execute(DCmdSource source, TRAPS);
 };
 
+class TouchedMethodsDCmd : public DCmdWithParser {
+public:
+  TouchedMethodsDCmd(outputStream* output, bool heap);
+  static const char* name() {
+    return "VM.print_touched_methods";
+  }
+  static const char* description() {
+    return "Print all methods that have ever been touched during the lifetime of this JVM.";
+  }
+  static const char* impact() {
+    return "Medium: Depends on Java content.";
+  }
+  static int num_arguments();
+  virtual void execute(DCmdSource source, TRAPS);
+};
+
 // See also: thread_dump in attachListener.cpp
 class ThreadDumpDCmd : public DCmdWithParser {
 protected:
diff --git a/hotspot/test/runtime/CommandLine/PrintTouchedMethods.java b/hotspot/test/runtime/CommandLine/PrintTouchedMethods.java
new file mode 100644
index 000000000..c85b2a5d8
--- /dev/null
+++ b/hotspot/test/runtime/CommandLine/PrintTouchedMethods.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8025692
+ * @modules java.base/jdk.internal.misc
+ *          java.management
+ * @library /testlibrary
+ * @compile TestLogTouchedMethods.java PrintTouchedMethods.java
+ * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+LogTouchedMethods PrintTouchedMethods
+ */
+
+import java.io.File;
+import java.util.List;
+import com.oracle.java.testlibrary.*;
+
+public class PrintTouchedMethods {
+
+    public static void main(String args[]) throws Exception {
+      ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(
+          "-XX:-UnlockDiagnosticVMOptions",
+          "-XX:+LogTouchedMethods",
+          "-XX:+PrintTouchedMethodsAtExit",
+          "TestLogTouchedMethods");
+
+      // UnlockDiagnostic turned off, should fail
+      OutputAnalyzer output = new OutputAnalyzer(pb.start());
+      output.shouldContain("Error: VM option 'LogTouchedMethods' is diagnostic and must be enabled via -XX:+UnlockDiagnosticVMOptions.");
+      output.shouldContain("Error: Could not create the Java Virtual Machine.");
+
+      pb = ProcessTools.createJavaProcessBuilder(
+          "-XX:+UnlockDiagnosticVMOptions",
+          "-XX:+LogTouchedMethods",
+          "-XX:+PrintTouchedMethodsAtExit",
+          "TestLogTouchedMethods");
+      output = new OutputAnalyzer(pb.start());
+      // check order:
+      // 1 "# Method::print_touched_methods version 1" is the first in first line
+      // 2 should contain TestLogMethods.methodA:()V
+      // 3 should not contain TestLogMethods.methodB:()V
+      // Repeat above for another run with -Xint
+      List<String> lines = output.asLines();
+
+      if (lines.size() < 1) {
+        throw new Exception("Empty output");
+      }
+
+      String first = lines.get(0);
+      if (!first.equals("# Method::print_touched_methods version 1")) {
+        throw new Exception("First line mismatch");
+      }
+
+      output.shouldContain("TestLogTouchedMethods.methodA:()V");
+      output.shouldNotContain("TestLogTouchedMethods.methodB:()V");
+      output.shouldHaveExitValue(0);
+
+      pb = ProcessTools.createJavaProcessBuilder(
+          "-XX:+UnlockDiagnosticVMOptions",
+          "-Xint",
+          "-XX:+LogTouchedMethods",
+          "-XX:+PrintTouchedMethodsAtExit",
+          "TestLogTouchedMethods");
+      output = new OutputAnalyzer(pb.start());
+      lines = output.asLines();
+
+      if (lines.size() < 1) {
+        throw new Exception("Empty output");
+      }
+
+      first = lines.get(0);
+      if (!first.equals("# Method::print_touched_methods version 1")) {
+        throw new Exception("First line mismatch");
+      }
+
+      output.shouldContain("TestLogTouchedMethods.methodA:()V");
+      output.shouldNotContain("TestLogTouchedMethods.methodB:()V");
+      output.shouldHaveExitValue(0);
+
+      pb = ProcessTools.createJavaProcessBuilder(
+          "-XX:+UnlockDiagnosticVMOptions",
+          "-Xint",
+          "-XX:+LogTouchedMethods",
+          "-XX:+PrintTouchedMethodsAtExit",
+          "-XX:-TieredCompilation",
+          "TestLogTouchedMethods");
+      output = new OutputAnalyzer(pb.start());
+      lines = output.asLines();
+
+      if (lines.size() < 1) {
+        throw new Exception("Empty output");
+      }
+
+      first = lines.get(0);
+      if (!first.equals("# Method::print_touched_methods version 1")) {
+        throw new Exception("First line mismatch");
+      }
+
+      output.shouldContain("TestLogTouchedMethods.methodA:()V");
+      output.shouldNotContain("TestLogTouchedMethods.methodB:()V");
+      output.shouldHaveExitValue(0);
+
+      // Test jcmd PrintTouchedMethods VM.print_touched_methods
+      String pid = Long.toString(ProcessTools.getProcessId());
+      pb = new ProcessBuilder();
+      pb.command(new String[] {JDKToolFinder.getJDKTool("jcmd"), pid, "VM.print_touched_methods"});
+      output = new OutputAnalyzer(pb.start());
+      try {
+        output.shouldContain("PrintTouchedMethods.main:([Ljava/lang/String;)V");
+      } catch (RuntimeException e) {
+        output.shouldContain("Unknown diagnostic command");
+      }
+  }
+}
\ No newline at end of file
diff --git a/hotspot/test/runtime/CommandLine/TestLogTouchedMethods.java b/hotspot/test/runtime/CommandLine/TestLogTouchedMethods.java
new file mode 100644
index 000000000..655d4a916
--- /dev/null
+++ b/hotspot/test/runtime/CommandLine/TestLogTouchedMethods.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* used by PrintTouchedMethods.java */
+public class TestLogTouchedMethods {
+  public static void main(String[] args) {
+    new TestLogTouchedMethods().methodA();
+  }
+
+  // methods without return values and local variables are optimized in interpreter mode
+  // it can not be recorded
+  // leave at least one local variable
+  public void methodA() { int a = 0; } // called
+  public void methodB() { int b = 0; } // this should not be called
+}
-- 
2.22.0

