8370102: Print method signatures in edge cases when using TraceBytecodes

Reviewed-by: coleenp, dholmes
This commit is contained in:
Paul Hübner 2026-04-22 12:21:14 +00:00
parent c796ed1ad8
commit 21b9a50239
7 changed files with 325 additions and 72 deletions

View File

@ -40,21 +40,17 @@
#include "utilities/align.hpp"
// Prints the current bytecode and its attributes using bytecode-specific information.
// Printing bytecodes is not an idempotent operation, as this relies on modifying state.
// An instance of this class is thus intended to only print a single bytecode. All state
// shared between multiple bytecodes needs to be in BytecodeTracerData.
class BytecodePrinter {
private:
// %%% This field is not GC-ed, and so can contain garbage
// between critical sections. Use only pointer-comparison
// operations on the pointer, except within a critical section.
// (Also, ensure that occasional false positives are benign.)
Method* _current_method;
bool _is_wide;
Bytecodes::Code _code;
address _next_pc; // current decoding position
int _flags;
bool _use_cp_cache;
BytecodeTracerData* _data;
int _flags;
Bytecodes::Code _raw_code; // note: some methods translate this to the Java bytecode
address _next_pc; // current decoding position, destructive
bool use_cp_cache() const { return _use_cp_cache; }
void align() { _next_pc = align_up(_next_pc, sizeof(jint)); }
int get_byte() { return *(jbyte*) _next_pc++; } // signed
int get_index_u1() { return *(address)_next_pc++; } // returns 0x00 - 0xff as an int
@ -65,11 +61,22 @@ class BytecodePrinter {
int get_Java_index_u2() { int i = Bytes::get_Java_u2 (_next_pc); _next_pc += 2; return i; }
int get_Java_index_u4() { int i = Bytes::get_Java_u4 (_next_pc); _next_pc += 4; return i; }
int get_index_special() { return (is_wide()) ? get_Java_index_u2() : get_index_u1(); }
Method* method() const { return _current_method; }
bool is_wide() const { return _is_wide; }
Bytecodes::Code raw_code() const { return Bytecodes::Code(_code); }
ConstantPool* constants() const { return method()->constants(); }
ConstantPoolCache* cpcache() const { assert(use_cp_cache(), "must be"); return constants()->cache(); }
// Warning: only do pointer comparison between critical sections.
Method* method() const { return _data->current_method(); }
void set_method(Method* current) { _data->set_current_method(current); }
// Warning: should only be used for comparison and not dereferenced.
intptr_t* fp() const { return _data->current_fp(); }
void set_fp(intptr_t* current) { _data->set_current_fp(current); }
bool is_wide() const { return _data->is_wide(); }
void set_wide(bool wide) { _data->set_wide(wide); }
ConstantPool* constants() const { return method()->constants(); }
// This may be called during linking after bytecodes are rewritten to point to the cpCache.
bool use_cp_cache() const { return constants()->cache() != nullptr; }
ConstantPoolCache* cpcache() const { assert(use_cp_cache(), "must be"); return constants()->cache(); }
void print_constant(int i, outputStream* st);
void print_cpcache_entry(int cpc_index, outputStream* st);
@ -81,35 +88,32 @@ class BytecodePrinter {
void print_method_data_at(int bci, outputStream* st);
public:
BytecodePrinter(int flags = 0) : _is_wide(false), _code(Bytecodes::_illegal), _flags(flags) {}
BytecodePrinter(BytecodeTracerData* data, int flags = 0) :
_data(data),
_flags(flags),
_raw_code(Bytecodes::_illegal),
_next_pc(nullptr) {}
#ifndef PRODUCT
BytecodePrinter(Method* prev_method) : BytecodePrinter(0) {
_current_method = prev_method;
}
// This method is called while executing the raw bytecodes, so none of
// the adjustments that BytecodeStream performs applies.
void trace(const methodHandle& method, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st) {
void trace(const methodHandle& method, intptr_t* fp, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st) {
assert(_raw_code == Bytecodes::_illegal, "invariant");
ResourceMark rm;
bool method_changed = _current_method != method();
_current_method = method();
_use_cp_cache = method->constants()->cache() != nullptr;
// Method changes can be another method getting called, or a self-recursive call.
bool method_changed = (this->method() != method()) || (this->fp() != fp);
set_method(method());
set_fp(fp);
assert(method->method_holder()->is_linked(),
"this function must be called on methods that are already executing");
// If the method changed (new method call, return to previous method after call finishes),
// the signature needs to be re-printed for interpretability.
if (method_changed) {
// Note 1: This code will not work as expected with true MT/MP.
// Need an explicit lock or a different solution.
// It is possible for this block to be skipped, if a garbage
// _current_method pointer happens to have the same bits as
// the incoming method. We could lose a line of trace output.
// This is acceptable in a debug-only feature.
st->cr();
st->print("[%zu] ", Thread::current()->osthread()->thread_id_for_printing());
method->print_name(st);
st->cr();
}
Bytecodes::Code code;
if (is_wide()) {
// bcp wasn't advanced if previous bytecode was _wide.
@ -117,14 +121,13 @@ class BytecodePrinter {
} else {
code = Bytecodes::code_at(method(), bcp);
}
_code = code;
_raw_code = code;
_next_pc = is_wide() ? bcp+2 : bcp+1;
bool is_terminal = Bytecodes::is_terminal(code);
// Trace each bytecode unless we're truncating the tracing output, then only print the first
// bytecode in every method as well as returns/throws that pop control flow
if (!TraceBytecodesTruncated || method_changed ||
code == Bytecodes::_athrow ||
code == Bytecodes::_return_register_finalizer ||
(code >= Bytecodes::_ireturn && code <= Bytecodes::_return)) {
// bytecode in every method as well as returns/throws that pop control flow.
if (!TraceBytecodesTruncated || method_changed || is_terminal) {
int bci = (int)(bcp - method->code_base());
st->print("[%zu] ", Thread::current()->osthread()->thread_id_for_printing());
if (Verbose) {
@ -138,8 +141,16 @@ class BytecodePrinter {
}
// Set is_wide for the next one, since the caller of this doesn't skip
// the next bytecode.
_is_wide = (code == Bytecodes::_wide);
_code = Bytecodes::_illegal;
set_wide(code == Bytecodes::_wide);
// Finished using the code, reset to illegal.
_raw_code = Bytecodes::_illegal;
// Invalidate the current method to force a signature change. In some
// rare cases, the method and frame pointers aren't enough to determine
// a new method invocation, so this ensures the signature is re-printed.
if (is_terminal) {
set_method(nullptr);
}
if (TraceBytecodesStopAt != 0 && BytecodeCounter::counter_value() >= TraceBytecodesStopAt) {
TraceBytecodes = false;
@ -150,17 +161,16 @@ class BytecodePrinter {
// Used for Method::print_codes(). The input bcp comes from
// BytecodeStream, which will skip wide bytecodes.
void trace(const methodHandle& method, address bcp, outputStream* st) {
_current_method = method();
// This may be called during linking after bytecodes are rewritten to point to the cpCache.
_use_cp_cache = method->constants()->cache() != nullptr;
assert(_raw_code == Bytecodes::_illegal, "invariant");
set_method(method());
ResourceMark rm;
Bytecodes::Code code = Bytecodes::code_at(method(), bcp);
// Set is_wide
_is_wide = (code == Bytecodes::_wide);
set_wide(code == Bytecodes::_wide);
if (is_wide()) {
code = Bytecodes::code_at(method(), bcp+1);
}
_code = code;
_raw_code = code;
int bci = (int)(bcp - method->code_base());
// Print bytecode index and name
if (ClassPrinter::has_mode(_flags, ClassPrinter::PRINT_BYTECODE_ADDR)) {
@ -180,26 +190,20 @@ class BytecodePrinter {
};
#ifndef PRODUCT
// We need a global instance to keep track of the method being printed so we can report that
// the method has changed. If this method is redefined and removed, that's ok because the method passed
// in won't match, and this will print the method passed in again. Racing threads changing this global
// will result in reprinting the method passed in again.
static Method* _method_currently_being_printed = nullptr;
void BytecodeTracer::trace_interpreter(const methodHandle& method, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st) {
void BytecodeTracer::trace_interpreter(const methodHandle& method, intptr_t* fp, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st) {
if (TraceBytecodes && BytecodeCounter::counter_value() >= TraceBytecodesAt) {
BytecodePrinter printer(AtomicAccess::load_acquire(&_method_currently_being_printed));
stringStream buf;
printer.trace(method, bcp, tos, tos2, &buf);
st->print("%s", buf.freeze());
// Save method currently being printed to detect when method printing changes.
AtomicAccess::release_store(&_method_currently_being_printed, method());
BytecodeTracerData* data = JavaThread::current()->bytecode_tracer_data();
BytecodePrinter printer(data);
printer.trace(method, fp, bcp, tos, tos2, st);
}
}
#endif
void BytecodeTracer::print_method_codes(const methodHandle& method, int from, int to, outputStream* st, int flags, bool buffered) {
BytecodePrinter method_printer(flags);
// Debug builds can't re-use the data in the Java Thread as that is used for tracing
// the current bytecodes, rather than to print diagnostic information as is the
// case here. Always stack-allocate for this printing.
BytecodeTracerData data;
BytecodeStream s(method);
s.set_interval(from, to);
@ -207,6 +211,7 @@ void BytecodeTracer::print_method_codes(const methodHandle& method, int from, in
stringStream ss;
outputStream* out = buffered ? &ss : st;
while (s.next() >= 0) {
BytecodePrinter method_printer(&data, flags);
method_printer.trace(method, s.bcp(), out);
}
if (buffered) {
@ -345,7 +350,7 @@ void BytecodePrinter::print_bsm(int cp_index, outputStream* st) {
void BytecodePrinter::print_attributes(int bci, outputStream* st) {
// Show attributes of pre-rewritten codes
Bytecodes::Code code = Bytecodes::java_code(raw_code());
Bytecodes::Code code = Bytecodes::java_code(_raw_code);
// If the code doesn't have any fields there's nothing to print.
// note this is ==1 because the tableswitch and lookupswitch are
// zero size (for some reason) and we want to print stuff out for them.
@ -366,7 +371,7 @@ void BytecodePrinter::print_attributes(int bci, outputStream* st) {
case Bytecodes::_ldc:
{
int cp_index;
if (Bytecodes::uses_cp_cache(raw_code())) {
if (Bytecodes::uses_cp_cache(_raw_code)) {
assert(use_cp_cache(), "fast ldc bytecode must be in linked classes");
int obj_index = get_index_u1();
cp_index = constants()->object_to_cp_index(obj_index);
@ -381,7 +386,7 @@ void BytecodePrinter::print_attributes(int bci, outputStream* st) {
case Bytecodes::_ldc2_w:
{
int cp_index;
if (Bytecodes::uses_cp_cache(raw_code())) {
if (Bytecodes::uses_cp_cache(_raw_code)) {
assert(use_cp_cache(), "fast ldc bytecode must be in linked classes");
int obj_index = get_native_index_u2();
cp_index = constants()->object_to_cp_index(obj_index);
@ -533,7 +538,7 @@ void BytecodePrinter::print_attributes(int bci, outputStream* st) {
cp_index = method_entry->constant_pool_index();
print_field_or_method(cp_index, st);
if (raw_code() == Bytecodes::_invokehandle &&
if (_raw_code == Bytecodes::_invokehandle &&
ClassPrinter::has_mode(_flags, ClassPrinter::PRINT_METHOD_HANDLE)) {
assert(use_cp_cache(), "invokehandle is only in rewritten methods");
method_entry->print_on(st);

View File

@ -25,21 +25,54 @@
#ifndef SHARE_INTERPRETER_BYTECODETRACER_HPP
#define SHARE_INTERPRETER_BYTECODETRACER_HPP
#include "interpreter/bytecodes.hpp"
#include "memory/allStatic.hpp"
#include "utilities/globalDefinitions.hpp"
// The BytecodeTracer is a helper class used by the interpreter for run-time
// bytecode tracing. If TraceBytecodes turned on, trace_interpreter() will be called
// for each bytecode.
class Method;
class methodHandle;
class outputStream;
class BytecodeClosure;
// The BytecodeTracer is a helper class used by the interpreter for run-time
// bytecode tracing. If TraceBytecodes is turned on, trace_interpreter() will be called
// for each bytecode.
class BytecodeTracer: AllStatic {
public:
NOT_PRODUCT(static void trace_interpreter(const methodHandle& method, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st);)
NOT_PRODUCT(static void trace_interpreter(const methodHandle& method, intptr_t* fp, address bcp, uintptr_t tos, uintptr_t tos2, outputStream* st);)
static void print_method_codes(const methodHandle& method, int from, int to, outputStream* st, int flags, bool buffered = true);
};
// Provides tracing-centric context whose lifespan exceeds the printing of
// a single bytecode. For instance, it is needed to determine method switches
// in order to print the appropriate signature once a switch happens.
class BytecodeTracerData {
private:
Method* _current_method; // for method switches
intptr_t* _current_fp; // for self-recursion
bool _is_wide; // to parse the next bytecode properly
public:
BytecodeTracerData() : _current_method(nullptr),
_current_fp(nullptr),
_is_wide(false) {}
// The current method may point to a stale/garbage Method. While pointer
// comparison is safe, it should only be dereferenced while guaranteed to
// be valid. For example, if the current method is set to the result of a
// methodHandle call, current_method() may be dereferenced while the handle
// is live. It is always up to the caller to ensure that current_method()
// is safe to dereference.
Method* current_method() const { return _current_method; }
void set_current_method(Method* current) { _current_method = current; }
// The frame pointer should only ever be used for pointer comparison and may
// never be dereferenced.
intptr_t* current_fp() const { return _current_fp; }
void set_current_fp(intptr_t* current) { _current_fp = current; }
bool is_wide() const { return _is_wide; }
void set_wide(bool wide) { _is_wide = wide; }
};
#endif // SHARE_INTERPRETER_BYTECODETRACER_HPP

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2026, 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
@ -418,6 +418,9 @@ class Bytecodes: AllStatic {
static bool is_zero_const (Code code) { return (code == _aconst_null || code == _iconst_0
|| code == _fconst_0 || code == _dconst_0); }
static bool is_return (Code code) { return (_ireturn <= code && code <= _return); }
static bool is_terminal (Code code) { return is_return(code) ||
code == _return_register_finalizer ||
code == _athrow; }
static bool is_invoke (Code code) { return (_invokevirtual <= code && code <= _invokedynamic); }
static bool is_field_code (Code code) { return (_getstatic <= java_code(code) && java_code(code) <= _putfield); }
static bool has_receiver (Code code) { assert(is_invoke(code), ""); return code == _invokevirtual ||

View File

@ -1517,7 +1517,9 @@ JRT_LEAF(intptr_t, InterpreterRuntime::trace_bytecode(JavaThread* current, intpt
LastFrameAccessor last_frame(current);
assert(last_frame.is_interpreted_frame(), "must be an interpreted frame");
methodHandle mh(current, last_frame.method());
BytecodeTracer::trace_interpreter(mh, last_frame.bcp(), tos, tos2, tty);
stringStream st;
BytecodeTracer::trace_interpreter(mh, last_frame.get_frame().real_fp(), last_frame.bcp(), tos, tos2, &st);
tty->print("%s", st.freeze());
return preserve_this_value;
JRT_END
#endif // !PRODUCT

View File

@ -37,6 +37,7 @@
#include "gc/shared/oopStorage.hpp"
#include "gc/shared/oopStorageSet.hpp"
#include "gc/shared/tlab_globals.hpp"
#include "interpreter/bytecodeTracer.hpp"
#include "jfr/jfrEvents.hpp"
#include "jvm.h"
#include "jvmtifiles/jvmtiEnv.hpp"
@ -432,6 +433,8 @@ JavaThread::JavaThread(MemTag mem_tag) :
_visited_for_critical_count(false),
#endif
NOT_PRODUCT(_bytecode_tracer_data{} COMMA)
_terminated(_not_terminated),
_in_deopt_handler(0),
_doing_unsafe_access(false),

View File

@ -26,6 +26,9 @@
#ifndef SHARE_RUNTIME_JAVATHREAD_HPP
#define SHARE_RUNTIME_JAVATHREAD_HPP
#ifndef PRODUCT
#include "interpreter/bytecodeTracer.hpp"
#endif // PRODUCT
#include "jni.h"
#include "memory/allocation.hpp"
#include "oops/oop.hpp"
@ -291,6 +294,16 @@ class JavaThread: public Thread {
}
#endif // ASSERT
#ifndef PRODUCT
private:
BytecodeTracerData _bytecode_tracer_data;
public:
BytecodeTracerData* bytecode_tracer_data() {
return &_bytecode_tracer_data;
}
#endif // PRODUCT
// JavaThread termination support
public:
enum TerminatedTypes {

View File

@ -0,0 +1,194 @@
/*
* Copyright (c) 2026, 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 8370102
* @requires vm.debug
* @library / /test/lib
* @summary Test to ensure the signatures in -XX:+TraceBytecodes are printed in edge cases
* @run driver TraceBytecodesSignatures
*/
import java.io.IOException;
import java.util.List;
import jdk.test.lib.Utils;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
public class TraceBytecodesSignatures {
public static void main(String[] args)
throws InterruptedException, IOException {
if (args.length == 1 && "worker".equals(args[0])) {
runTest();
return;
}
// Enable byteocde tracing but disable compilation of the key methods
// that we will trace.
String[] processArgs = new String[] {
"-XX:+TraceBytecodes",
exclude("runTest"),
exclude("testSameMethod"),
exclude("testSameMethodHelper"),
excludeConstructor(),
exclude("testSelfRecursive"),
exclude("testSelfRecursiveHelper"),
klass(),
"worker"
};
// Create a VM process and trace its bytecodes.
ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(processArgs);
OutputAnalyzer oa = new OutputAnalyzer(pb.start())
.shouldHaveExitValue(0);
analyze(oa.stdoutAsLines());
}
private static void runTest() {
testSameMethod(true);
testSameMethod(false);
new TestConstructor();
new TestConstructor();
testSelfRecursive(3);
}
// Expect 3 signature prints:
// 1. The first call
// 2. The first call switch back after the helper is called
// 3. The second call
private static final int EXPECTED_SAME_METHOD = 3;
// Expect 2 signature prints:
// 1. The first call of the no-arg constructor
// 2. Switch back after calling the 1-arg constructor
// 3. The second call of the no-arg constructor
// 4. Switch back again
private static final int EXPECTED_CONSTRUCTOR = 4;
// Expect 10 signature prints:
// 1. The first recursive call
// 2. The first switch back after the helper is called.
// 3. The second recursive call
// 4. The second switch back
// 5. The third recursive call
// 6. The third switch back
// 7. The fourth recursive call: base case hit
// 8-10. Switch back to i=1, i=2, i=3, respectively
private static final int EXPECTED_SELF_RECURSIVE_METHOD = 10;
// Ensure that the expected signatures are printed the correct nr of times.
// The signatures here need to be changed if any of the test code is changed.
private static void analyze(List<String> lines) {
System.out.println("Analyzing " + lines.size() + " lines");
int testSameMethodCount = 0;
int testConstructorCount = 0;
int testSelfRecursiveCount = 0;
for (String line : lines) {
if (line.contains("static void TraceBytecodesSignatures.testSameMethod(jboolean)")) {
testSameMethodCount++;
} else if (line.contains("virtual void TraceBytecodesSignatures$TestConstructor.<init>()")) {
testConstructorCount++;
} else if (line.contains("static void TraceBytecodesSignatures.testSelfRecursive(jint)")) {
testSelfRecursiveCount++;
}
}
if (testSameMethodCount != EXPECTED_SAME_METHOD) {
throw new RuntimeException("testSameMethod: "
+ EXPECTED_SAME_METHOD
+ " != "
+ testSameMethodCount);
}
if (testConstructorCount != EXPECTED_CONSTRUCTOR) {
throw new RuntimeException("testConstructor: "
+ EXPECTED_CONSTRUCTOR
+ " != "
+ testConstructorCount);
}
if (testSelfRecursiveCount != EXPECTED_SELF_RECURSIVE_METHOD) {
throw new RuntimeException("testSelfRecursive: "
+ EXPECTED_SELF_RECURSIVE_METHOD
+ " != "
+ testSelfRecursiveCount);
}
}
// Use a variable to do some arithmetic on to represent work.
// This work should not produce method invocations, hence integer arithmetic.
private static int globalState = 0;
private static void testSameMethod(boolean other) {
globalState = 1;
if (other) {
testSameMethodHelper();
}
globalState = 1;
}
private static void testSameMethodHelper() {
globalState = 5;
}
private static final class TestConstructor {
// Represents some kind of mutable state, not necessarily a object attribute.
private static String foo = "test";
public TestConstructor() {
this("bar");
}
public TestConstructor(String other) {
foo = other;
}
}
private static void testSelfRecursive(int i) {
if (i == 0) {
return;
}
globalState += 2;
// Ensure to generate another method call within the recursive method.
// This will trigger the method switches more often.
testSelfRecursiveHelper();
globalState -= 2;
testSelfRecursive(i - 1);
}
private static void testSelfRecursiveHelper() {
globalState = -1;
}
// CompileCommand is accepted even if the VM does not use JIT compilation.
private static String exclude(String methodName) {
return "-XX:CompileCommand=exclude," + klass() + "." + methodName;
}
private static String excludeConstructor() {
return "-XX:CompileCommand=exclude," + klass() + "$TestConstructor.<init>";
}
private static String klass() {
return TraceBytecodesSignatures.class.getName();
}
}