Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/hotspot/share/classfile/javaClasses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3588,6 +3588,7 @@ int java_lang_reflect_Field::_modifiers_offset;
int java_lang_reflect_Field::_trusted_final_offset;
int java_lang_reflect_Field::_signature_offset;
int java_lang_reflect_Field::_annotations_offset;
int java_lang_reflect_Field::slot_mask = (1 << 17) - 1;

#define FIELD_FIELDS_DO(macro) \
macro(_clazz_offset, k, vmSymbols::clazz_name(), class_signature, false); \
Expand Down Expand Up @@ -3645,10 +3646,13 @@ void java_lang_reflect_Field::set_type(oop field, oop value) {
}

int java_lang_reflect_Field::slot(oop reflect) {
return reflect->int_field(_slot_offset);
// We use a mask because JFR is encoding
// information in the most significant bits.
return reflect->int_field(_slot_offset) & slot_mask;
}

void java_lang_reflect_Field::set_slot(oop reflect, int value) {
assert(value <= slot_mask, "slot value is too big");
reflect->int_field_put(_slot_offset, value);
}

Expand Down
2 changes: 2 additions & 0 deletions src/hotspot/share/classfile/javaClasses.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,8 @@ class java_lang_reflect_Field : public java_lang_reflect_AccessibleObject {
static int _signature_offset;
static int _annotations_offset;

static int slot_mask;

static void compute_offsets();

public:
Expand Down
3 changes: 3 additions & 0 deletions src/hotspot/share/include/jvm.h
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ JVM_FindScopedValueBindings(JNIEnv *env, jclass threadClass);
JNIEXPORT jlong JNICALL
JVM_GetNextThreadIdOffset(JNIEnv *env, jclass threadClass);

JNIEXPORT jlong JNICALL
JVM_GetJfrEpochGenerationOffset(JNIEnv* env, jclass clazz);

/*
* jdk.internal.vm.Continuation
*/
Expand Down
5 changes: 5 additions & 0 deletions src/hotspot/share/jfr/jfr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "jfr/jni/jfrJavaSupport.hpp"
#include "jfr/leakprofiler/leakProfiler.hpp"
#include "jfr/recorder/checkpoint/jfrCheckpointManager.hpp"
#include "jfr/recorder/checkpoint/types/traceid/jfrTraceIdEpoch.hpp"
#include "jfr/recorder/jfrRecorder.hpp"
#include "jfr/recorder/repository/jfrEmergencyDump.hpp"
#include "jfr/recorder/repository/jfrRepository.hpp"
Expand Down Expand Up @@ -181,6 +182,10 @@ void Jfr::on_report_java_out_of_memory() {
}
}

int64_t Jfr::epoch_generation_offset() {
return reinterpret_cast<int64_t>(JfrTraceIdEpoch::epoch_generation_address());
}

#if INCLUDE_CDS
void Jfr::on_restoration(const Klass* k, JavaThread* jt) {
assert(k != nullptr, "invariant");
Expand Down
1 change: 1 addition & 0 deletions src/hotspot/share/jfr/jfr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class Jfr : AllStatic {
static bool has_sample_request(JavaThread* jt);
static void check_and_process_sample_request(JavaThread* jt);
static void on_report_java_out_of_memory();
static int64_t epoch_generation_offset();
CDS_ONLY(static void on_restoration(const Klass* k, JavaThread* jt);)
};

Expand Down
8 changes: 8 additions & 0 deletions src/hotspot/share/prims/jvm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2890,6 +2890,14 @@ JVM_ENTRY(jlong, JVM_GetNextThreadIdOffset(JNIEnv* env, jclass threadClass))
return ThreadIdentifier::unsafe_offset();
JVM_END

JVM_ENTRY(jlong, JVM_GetJfrEpochGenerationOffset(JNIEnv* env, jclass clazz))
#if INCLUDE_JFR
return Jfr::epoch_generation_offset();
#else
return 0;
#endif
JVM_END

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
ThreadsListHandle tlh(thread);
JavaThread* receiver = nullptr;
Expand Down
7 changes: 3 additions & 4 deletions src/java.base/share/classes/java/lang/reflect/Field.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 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
Expand Down Expand Up @@ -74,7 +74,6 @@
public final
class Field extends AccessibleObject implements Member {
private final Class<?> clazz;
private final int slot;
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private final String name;
Expand All @@ -91,6 +90,7 @@ class Field extends AccessibleObject implements Member {
* Some lazily initialized immutable states can be stored on root and shared to the copies.
*/
private Field root;
private int slot; // used also for Jfr epoch encoding
private transient volatile FieldRepository genericInfo;
private @Stable FieldAccessor fieldAccessor; // access control enabled
private @Stable FieldAccessor overrideFieldAccessor; // access control suppressed
Expand Down Expand Up @@ -1537,8 +1537,7 @@ private void postSetFinal(Class<?> caller, boolean unreflect) {
VM.initialErr().println(sb);
}

// record JFR event
FinalFieldMutationEvent.offer(getDeclaringClass(), getName());
FinalFieldMutationEvent.offer(root, root.slot);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 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
Expand All @@ -24,22 +24,51 @@
*/
package jdk.internal.event;

import java.lang.reflect.Field;

public class FinalFieldMutationEvent extends Event {
/**
* We throttle FinalFieldMutation JFR events by generating only one event per field
* during each JFR epoch. This is accomplished by encoding the JFR epoch generation (15 bits)
* into the most significant bits of the root Field object's slot field, using a CAS operation.
*
* An event is emitted if enabled, the epoch encoded in the slot field differs from the current epoch,
* and a thread successfully updates the slot field's epoch to the current epoch.
*
* Borrowing the most significant 15 bits from the slot integer field relies on the premise
* that the maximum number of fields encoded in the class file format is 65,535.
* In addition, with 17 bits available for slot encoding, there remains considerable capacity
* for VM-injected fields, allowing for a theoretical maximum of 131,072 slots.
*/
static final long SLOT_OFFSET = JfrEpochGeneration.getFieldOffset(Field.class, "slot");

public Class<?> declaringClass;
public String fieldName;

/**
* Commit a FinalFieldMutationEvent if enabled.
*/
public static void offer(Class<?> declaringClass, String fieldName) {
if (enabled()) {
public static void offer(Field root, int slot) {
if (shouldEmit(root, slot)) {
var event = new FinalFieldMutationEvent();
event.declaringClass = declaringClass;
event.fieldName = fieldName;
event.declaringClass = root.getDeclaringClass();
event.fieldName = root.getName();
event.commit();
}
}

private static boolean shouldEmit(Field root, int slot) {
if (enabled()) {
int currentEpoch = JfrEpochGeneration.getCurrentEpoch();
if (currentEpoch != (slot >> JfrEpochGeneration.EPOCH_SHIFT_INT)) {
int newRootFieldSlotEpoch =
currentEpoch << JfrEpochGeneration.EPOCH_SHIFT_INT | (slot & JfrEpochGeneration.VALUE_MASK_INT);
return JfrEpochGeneration.compareAndExchange(root, SLOT_OFFSET, slot, newRootFieldSlotEpoch);
Copy link
Copy Markdown
Member

@merykitty merykitty Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by-comment: Can you make slot non-final? I'm afraid there is a chance the JIT will not produce correct code for accesses into this field if it lies about its finality. See JDK-8379875.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave the slot final, and instead inject a dedicated field for the epoch value.

}
}
return false;
}

public static boolean enabled() {
// Generated by JFR
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
package jdk.internal.event;

import java.lang.reflect.Field;
import jdk.internal.misc.Unsafe;

class JfrEpochGeneration {
private static final Unsafe U;
private static final long EPOCH_GENERATION_OFFSET;

/**
* The JFR epoch generation is the range [1-32767],i.e. 15 bits.
*
* These constants are useful when operating on a field where
* the most significant bits are used to store a JFR epoch value.
*/
static final int EPOCH_SHIFT_INT = 17;
static final int VALUE_MASK_INT = (1 << EPOCH_SHIFT_INT) - 1;
static final int EPOCH_SHIFT_LONG = 49;
static final long VALUE_MASK_LONG = (1L << EPOCH_SHIFT_LONG) - 1;

static {
U = Unsafe.getUnsafe();
EPOCH_GENERATION_OFFSET = getJfrEpochGenerationOffset();
}

private static native long getJfrEpochGenerationOffset();

/**
* The JFR epoch generation is the range [1-32767],i.e. 15 bits.
*/
static short getCurrentEpoch() {
return U.getShort(EPOCH_GENERATION_OFFSET);
}

/**
* Get the address of a field to be used in cas operations.
*/
static long getFieldOffset(Class<?> clazz, String fieldName) {
return U.objectFieldOffset(clazz, fieldName);
}

static long getFieldOffset(Field field) {
return U.objectFieldOffset(field);
}

static long getStaticFielddOffset(Field field) {
return U.staticFieldOffset(field);
}

/**
* An epoch generation can be stored atomically into a field,
* perhaps together with some other value (use the shift and mask constants).
* Can be useful sometimes when committing events, to ensure only
* the setter will send an event, so as to limit events per epoch.
*/
static boolean compareAndExchange(Object o, long offset, int c, int v) {
return U.compareAndExchangeInt(o, offset, c, v) == c;
}

static boolean compareAndExchange(Object o, long offset, long c, long v) {
return U.compareAndExchangeLong(o, offset, c, v) == c;
}
}
34 changes: 34 additions & 0 deletions src/java.base/share/native/libjava/JfrEpochGeneration.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/

#include "jni.h"
#include "jvm.h"

#include "jdk_internal_event_JfrEpochGeneration.h"

JNIEXPORT jlong JNICALL
Java_jdk_internal_event_JfrEpochGeneration_getJfrEpochGenerationOffset(JNIEnv* env, jclass jfrEpochGen) {
return JVM_GetJfrEpochGenerationOffset(env, jfrEpochGen);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 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
Expand Down Expand Up @@ -34,7 +34,9 @@
import jdk.jfr.internal.RemoveFields;

@Category("Java Application")
@Label("Final Field Mutation")
@Label("Final Field Mutation Sample")
@Description("One event per field, per chunk, is recorded." +
"There might be more mutations, possibly with different stack traces")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description is "developer facing" and I wonder if this will be understandable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right - will reformulate it.

@Name(Type.EVENT_NAME_PREFIX + "FinalFieldMutation")
@RemoveFields("duration")
@StackFilter({
Expand Down
Loading