JDK Flight Recorder (JFR) provides support for custom events as a profiler. Around two years ago, I wrote a blog post on this very topic: Custom JFR Events: A Short Introduction. These custom events are beneficial because they enable us to record additional project-specific information alongside the standard JFR events, all in the same file. We can then view and process this information with the JFR tools. You can freely specify these events in Java.
There is only one tiny problem nobody talks about: Array support (and, in more general, the support of complex types).
Take the following event:
class ArrayEvent extends Event {
@Label("String Array")
String[] stringArray;
@Label("Int Array")
int[] intArray;
@Label("Long Array")
long[] longArray;
@Label("Non array field")
String nonArrayField = "default";
}
What would you expect to happen when we create the event using the following code?
ArrayEvent event = new ArrayEvent();
event.stringArray = new String[]{"one", "two", "three"};
event.intArray = new int[]{1, 2, 3, 4, 5};
event.longArray = new long[]{100L, 200L, 300L};
event.commit();
You probably expect that the event on disk looks like (via jfr print):
ArrayEvent {
startTime = 11:52:28.250 (2025-12-11)
stringArray = ["one", "two", "three"]
intArray = [1, 2, 3, 4, 5]
longArray = [100, 200, 300]
nonArrayField = "default"
eventThread = "main" (javaThreadId = 3)
stackTrace = [
JFRArrayTest.main(String[]) line: 30
]
}
Then you’re wrong. It actually looks like
ArrayEvent {
startTime = 11:52:28.250 (2025-12-11)
nonArrayField = "default"
eventThread = "main" (javaThreadId = 3)
stackTrace = [
JFRArrayTest.main(String[]) line: 30
]
}
Don’t trust me? Here is the source code, so that you can run it yourself. So essentially JFR hates arrays (kind of):

And no, the JVM doesn’t emit any warnings; it simply omits the fields.
Which Types are Supported?
The metadata.xml file, which defines many of the built-in JFR events, like GCHeapSummary, already provides some hints by listing the types at the end. However, I expected that arrays of supported types would be supported; however, as you saw above, this wasn’t the case.
When the JVM writes the fields of events, it checks that every explicit field has a valid type (source):
for (FieldModel field : classModel.fields()) {
if (!foundFields.contains(field.fieldName().stringValue()) &&
isValidField(field.flags().flagsMask(), field.fieldTypeSymbol())) {
fieldDescs.add(FieldDesc.of(field.fieldTypeSymbol(),
field.fieldName().stringValue()));
foundFields.add(field.fieldName().stringValue());
}
}
A slight tangent: The lines below this snippet show us that the parent class fields of our event are also parsed so that we can have hierarchies of custom events.
The isValidField method checks that fields are neither transient (marked not to be serialized) nor static (source):
static boolean isValidField(int access, String className) {
if (Modifier.isTransient(access) || Modifier.isStatic(access)) {
return false;
}
return Type.isValidJavaFieldType(className);
}
Skipping transient fields can be helpful when storing additional information in custom JFR events, when we reuse the event instances elsewhere.
The Type#isValidJavaFieldType method then checks that the type is one of: boolean, char, float, double, byte, short, int, long, Class, String, Thread, StackTrace (source)
As Erik Gahlin noted: From the jdk.jfr.Event documentation: Supported field types are the Java primitives: boolean, char, byte, short, int, long, float, and double. Supported reference types are: String, Thread and Class. Arrays, enums, and other reference types are silently ignored and not included. (not mentioning StackTrace)
So a custom field can have a non-primitive type if it’s one of the limited selections. Therefore, the following event is processed as expected:
class ClassEvent extends Event {
Class<?> klass;
StackTraceElement[] stack;
}
A sample event on disk may look like:
ArrayEvent {
startTime = 12:30:36.469 (2025-12-11)
nonArrayField = "default"
classField = java.lang.String (classLoader = bootstrap)
eventThread = "main" (javaThreadId = 3)
stackTrace = [
JFRArrayTest.main(String[]) line: 54
jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Object, Object[]) line: 104
java.lang.reflect.Method.invoke(Object, Object[]) line: 565
com.sun.tools.javac.launcher.SourceLauncher.execute(MemoryContext, String[]) line: 258
com.sun.tools.javac.launcher.SourceLauncher.run(String[], String[]) line: 138
]
}
Conclusion
Custom JFR events are a valuable tool, but they have subtle limitations, as illustrated in this blog post. It’s a pity that there are no warnings with unsupported types, so the issue is slightly more complex to catch. I found this while writing custom JFR events using the JMC JFR writer API and reading them back with the standard OpenJDK JFR API. Why did I do this?
Come back next week to learn about redacting sensitive information from JFR files.
This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone.





