Skip to main content

Java Profiling

CodeXray profiles Java applications via the Node Agent using a three-part approach: JVM attach, perfmap dumping, and eBPF stack sampling. The end result is a CPU FlameGraph in the Profiling tab with your Java method names properly resolved — at least, once one JVM flag is set.

The one flag you need

Add -XX:+PreserveFramePointer to your JVM options. Without it, your JIT-compiled Java methods will appear in the FlameGraph as unresolved hex addresses. With it, they resolve to com.yourcompany.Order.process style names.


How It Works​

When the Node Agent sees a Java process on the host:

  1. It attaches to the JVM via the attach socket (/tmp/.java_pid<N> or the equivalent inside the pod's namespace) using the Hotspot attach protocol.
  2. It asks the JVM to dump a perfmap — a file the JVM writes describing which JIT-compiled method lives at which memory address.
  3. The eBPF profiler samples stack traces the normal way. When it hits an address inside the JIT-compiled code region, it uses the perfmap to resolve that address back to a Java method name.
  4. Stacks ship to the backend where they're aggregated into the FlameGraph.
Not async-profiler

CodeXray does not ship or require async-profiler. It uses the JVM's built-in attach mechanism (jattach-style) and the JVM's own PrintAssembly / perfmap support — which every HotSpot JVM includes. This means no sidecar, no JAR to bundle, no extra library on the classpath.


Requirements​

JVM​

  • HotSpot / OpenJDK JVM — Oracle JDK, Adoptium Temurin, Amazon Corretto, Azul Zulu, and most mainstream distributions.
  • Java 8+ — older Javas may work but are not actively tested.
  • -XX:+PreserveFramePointer — required for JIT method-name resolution (see below).

Host​

  • The Node Agent running on the same host as the JVM process
  • Linux kernel 4.16+ (same as eBPF tracing)

Container access​

If your Java app runs in a container, the Node Agent handles namespace crossing automatically — nothing extra to configure on the container.


Enabling -XX:+PreserveFramePointer​

This one flag is the difference between a readable Java FlameGraph and a wall of hex addresses. Add it wherever your JVM options live.

Command line​

java -XX:+PreserveFramePointer -jar your-application.jar

As a JAVA_OPTS environment variable​

export JAVA_OPTS="-XX:+PreserveFramePointer"
java $JAVA_OPTS -jar your-application.jar

In a Dockerfile​

FROM eclipse-temurin:17-jre
COPY target/app.jar /app/app.jar
ENV JAVA_OPTS="-XX:+PreserveFramePointer"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

In a Kubernetes Pod spec​

apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-service
spec:
template:
spec:
containers:
- name: orders
image: your-registry/orders:latest
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:+PreserveFramePointer"

JAVA_TOOL_OPTIONS is the least intrusive — most frameworks (Spring Boot, Quarkus, Micronaut, Dropwizard, Tomcat) respect it without any additional wiring.

Spring Boot with JIB / native packaging​

Pass the flag through the standard mechanism your build tool provides — -Dspring-boot.run.jvmArguments="-XX:+PreserveFramePointer" for Maven, bootRun { jvmArgs += '-XX:+PreserveFramePointer' } for Gradle.


What Gets Profiled​

✓ Included​

  • JIT-compiled Java methods (with -XX:+PreserveFramePointer enabled)
  • JNI / native code called from Java
  • JVM internal frames — GC threads, compiler threads, JIT work, safepoints

✗ Not yet covered​

  • Memory allocations / heap profile — not available via eBPF
  • JVM GC pauses as first-class telemetry — use the Runtime tab on the application page for GC metrics
  • Lock contention / monitor wait profile — not captured

For a richer runtime view of a JVM service (GC stats, memory pools, threadpool state), the Runtime tab on the application page pairs well with the CPU FlameGraph here.


Verifying It's Working​

  1. Confirm the flag is active: on the running JVM, check jinfo -flag PreserveFramePointer <pid> — should show +PreserveFramePointer. In a container: docker exec <container> jinfo -flag PreserveFramePointer 1.
  2. Node Agent logs: the agent logs when it successfully attaches to a JVM and dumps a perfmap. Look for perfmap or jattach messages on the agent.
  3. In the UI: open the service → Profiling tab. The FlameGraph should show Java method names like com.yourcompany.service.OrderProcessor.process, not hex addresses.
Troubleshooting "unresolved Java frames"

If most frames are hex addresses or <unknown>:

  • The flag isn't set. Verify with jinfo as above.
  • The JVM is too old or not HotSpot — check your distribution.
  • The agent can't write to /tmp inside the container. Some hardened runtimes mount /tmp read-only — give the container a writable /tmp if possible.

Memory Profiling Status​

Memory profiles for Java applications are not currently available. eBPF can sample CPU stacks but can't observe Java heap allocations without JVMTI-level cooperation. For memory, the Runtime tab on the application page gives you heap usage, GC pauses, and memory-pool state, which covers most operational memory-investigation needs.


Next Steps​