eBPF Tracing for Java Processes in Java

eBPF (extended Berkeley Packet Filter) is a powerful Linux kernel technology that allows running sandboxed programs in the kernel space. This comprehensive guide covers using eBPF to trace and monitor Java processes from Java applications.

Understanding eBPF Architecture for Java

Key Components:

  • eBPF Programs: Kernel-space programs for tracing
  • eBPF Maps: Kernel data structures for communication
  • BPF Compiler Collection (BCC): Tools and libraries for eBPF
  • JVM TI (Tool Interface): Java-specific tracing integration
  • Perf Events: Linux performance monitoring interface

Project Setup and Dependencies

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JNA for native interface -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>
<!-- Java Native Runtime -->
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
<version>2.2.13</version>
</dependency>
<!-- Process utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<!-- JSON for data serialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>

Core eBPF Integration Framework

package com.example.ebpf;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Core eBPF integration for Java processes
*/
public class JavaEBPFTracer {
private static final String BPF_FS = "/sys/fs/bpf";
// JNA interface for Linux system calls
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int syscall(int number, Object... args);
int perf_event_open(PerfEventAttr attr, int pid, int cpu, 
int group_fd, long flags);
int ioctl(int fd, int request, Object... args);
int close(int fd);
}
// eBPF related constants
public static class BPFConstants {
public static final int BPF_MAP_TYPE_HASH = 1;
public static final int BPF_MAP_TYPE_ARRAY = 2;
public static final int BPF_MAP_TYPE_PERF_EVENT_ARRAY = 4;
public static final int BPF_FUNC_map_lookup_elem = 1;
public static final int BPF_FUNC_map_update_elem = 2;
public static final int BPF_FUNC_trace_printk = 6;
public static final int BPF_FUNC_perf_event_output = 25;
// System call numbers
public static final int SYS_bpf = 321;
public static final int SYS_perf_event_open = 298;
}
/**
* Main eBPF tracer class
*/
public static class JavaProcessTracer {
private final int targetPid;
private final Map<String, BPFProgram> loadedPrograms = new HashMap<>();
private final List<BPFMap> maps = new ArrayList<>();
private volatile boolean running = false;
public JavaProcessTracer(int pid) {
this.targetPid = pid;
validateProcess();
}
public JavaProcessTracer(String processName) {
this.targetPid = findPidByName(processName);
validateProcess();
}
private void validateProcess() {
if (targetPid <= 0) {
throw new IllegalArgumentException("Invalid PID: " + targetPid);
}
// Check if process exists and is a Java process
if (!isJavaProcess(targetPid)) {
throw new IllegalArgumentException("PID " + targetPid + " is not a Java process");
}
}
/**
* Load and attach eBPF programs
*/
public void startTracing() {
if (running) {
throw new IllegalStateException("Tracer already running");
}
try {
// Load common eBPF programs for Java monitoring
loadMethodTracingProgram();
loadGarbageCollectionProgram();
loadMemoryAllocationProgram();
loadThreadMonitoringProgram();
running = true;
System.out.println("Started eBPF tracing for Java PID: " + targetPid);
} catch (Exception e) {
throw new RuntimeException("Failed to start eBPF tracing", e);
}
}
/**
* Stop tracing and clean up
*/
public void stopTracing() {
if (!running) return;
try {
running = false;
// Detach and unload all programs
for (BPFProgram program : loadedPrograms.values()) {
program.detach();
}
loadedPrograms.clear();
// Clean up maps
for (BPFMap map : maps) {
map.close();
}
maps.clear();
System.out.println("Stopped eBPF tracing for Java PID: " + targetPid);
} catch (Exception e) {
System.err.println("Error stopping eBPF tracing: " + e.getMessage());
}
}
private void loadMethodTracingProgram() {
String programSource = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct method_entry_t {
u64 timestamp;
u32 pid;
u32 tid;
char class_name[64];
char method_name[64];
u64 duration_ns;
};
BPF_PERF_OUTPUT(method_events);
BPF_HASH(method_start, u32, u64);
int trace_method_entry(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
method_start.update(&tid, &ts);
return 0;
}
int trace_method_exit(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 *start_ts = method_start.lookup(&tid);
if (start_ts == 0) {
return 0; // No entry found
}
u64 duration = bpf_ktime_get_ns() - *start_ts;
struct method_entry_t event = {};
event.timestamp = bpf_ktime_get_ns();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.tid = tid;
event.duration_ns = duration;
// In real implementation, you'd extract class/method names
// from JVM structures or USDT probes
__builtin_memcpy(event.class_name, "UnknownClass", 13);
__builtin_memcpy(event.method_name, "unknownMethod", 14);
method_events.perf_submit(ctx, &event, sizeof(event));
method_start.delete(&tid);
return 0;
}
""";
BPFProgram program = new BPFProgram("method_tracing", programSource);
program.attachKprobe("method_entry", "trace_method_entry");
program.attachKprobe("method_exit", "trace_method_exit");
loadedPrograms.put("method_tracing", program);
}
private void loadGarbageCollectionProgram() {
String programSource = """
#include <uapi/linux/ptrace.h>
struct gc_event_t {
u64 timestamp;
u32 pid;
u32 gc_id;
u8 gc_type;
u64 pause_time_ns;
u64 memory_freed;
};
BPF_PERF_OUTPUT(gc_events);
int trace_gc_start(struct pt_regs *ctx) {
struct gc_event_t event = {};
event.timestamp = bpf_ktime_get_ns();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.gc_type = 1; // Young GC
gc_events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int trace_gc_end(struct pt_regs *ctx) {
// Track GC completion and statistics
return 0;
}
""";
BPFProgram program = new BPFProgram("gc_tracing", programSource);
// Attach to GC-related kernel functions or USDT probes
loadedPrograms.put("gc_tracing", program);
}
private void loadMemoryAllocationProgram() {
String programSource = """
#include <uapi/linux/ptrace.h>
struct allocation_event_t {
u64 timestamp;
u32 pid;
u32 tid;
u64 size;
u64 address;
u8 allocation_type;
};
BPF_PERF_OUTPUT(allocation_events);
int trace_malloc(struct pt_regs *ctx) {
struct allocation_event_t event = {};
event.timestamp = bpf_ktime_get_ns();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.tid = bpf_get_current_pid_tgid();
event.allocation_type = 1; // malloc
// Size would be in register or stack based on ABI
// event.size = PT_REGS_PARM1(ctx);
allocation_events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
""";
BPFProgram program = new BPFProgram("allocation_tracing", programSource);
program.attachKprobe("malloc", "trace_malloc");
loadedPrograms.put("allocation_tracing", program);
}
private void loadThreadMonitoringProgram() {
String programSource = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct thread_event_t {
u64 timestamp;
u32 pid;
u32 tid;
u8 event_type; // 0: start, 1: end, 2: park, 3: unpark
char thread_name[32];
};
BPF_PERF_OUTPUT(thread_events);
int trace_thread_start(struct pt_regs *ctx) {
struct thread_event_t event = {};
event.timestamp = bpf_ktime_get_ns();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.tid = bpf_get_current_pid_tgid();
event.event_type = 0;
thread_events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
""";
BPFProgram program = new BPFProgram("thread_tracing", programSource);
loadedPrograms.put("thread_tracing", program);
}
// Utility methods
private static int findPidByName(String processName) {
try {
Process process = Runtime.getRuntime().exec(new String[]{
"pgrep", "-f", processName
});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
return line != null ? Integer.parseInt(line.trim()) : -1;
}
} catch (Exception e) {
return -1;
}
}
private static boolean isJavaProcess(int pid) {
try {
Path exePath = Paths.get("/proc", String.valueOf(pid), "exe");
String exe = Files.readSymbolicLink(exePath).toString();
return exe.contains("java") || exe.contains("jvm");
} catch (Exception e) {
return false;
}
}
public boolean isRunning() {
return running;
}
public int getTargetPid() {
return targetPid;
}
}
/**
* Represents an eBPF program
*/
public static class BPFProgram {
private final String name;
private final String sourceCode;
private int programFd = -1;
private final List<Integer> attachedProbes = new ArrayList<>();
public BPFProgram(String name, String sourceCode) {
this.name = name;
this.sourceCode = sourceCode;
}
public void attachKprobe(String functionName, String probeFunction) {
// In real implementation, this would use bpf_attach_kprobe
System.out.println("Attaching kprobe " + probeFunction + " to " + functionName);
}
public void attachTracepoint(String category, String event, String probeFunction) {
// Attach to tracepoint
System.out.println("Attaching tracepoint " + category + ":" + event);
}
public void attachUSDT(String provider, String probeName) {
// Attach to USDT (User Statically Defined Tracing) probe
System.out.println("Attaching USDT probe " + provider + ":" + probeName);
}
public void detach() {
// Detach all probes and close program
for (Integer probeFd : attachedProbes) {
if (probeFd > 0) {
CLibrary.INSTANCE.close(probeFd);
}
}
attachedProbes.clear();
if (programFd > 0) {
CLibrary.INSTANCE.close(programFd);
programFd = -1;
}
}
public String getName() {
return name;
}
}
/**
* Represents an eBPF map
*/
public static class BPFMap {
private final String name;
private final int mapType;
private final int keySize;
private final int valueSize;
private final int maxEntries;
private int mapFd = -1;
public BPFMap(String name, int mapType, int keySize, int valueSize, int maxEntries) {
this.name = name;
this.mapType = mapType;
this.keySize = keySize;
this.valueSize = valueSize;
this.maxEntries = maxEntries;
}
public void open() {
// Open or create BPF map
}
public void close() {
if (mapFd > 0) {
CLibrary.INSTANCE.close(mapFd);
mapFd = -1;
}
}
public int getMapFd() {
return mapFd;
}
}
// Perf event attribute structure
public static class PerfEventAttr extends com.sun.jna.Structure {
public int type;
public int size;
public long config;
public long sample_period;
public long sample_type;
public long read_format;
public long disabled;
public long wakeup_events;
@Override
protected java.util.List<String> getFieldOrder() {
return java.util.Arrays.asList(
"type", "size", "config", "sample_period", "sample_type",
"read_format", "disabled", "wakeup_events"
);
}
}
}

JVM USDT Probes Integration

package com.example.ebpf;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
/**
* JVM USDT (User Statically Defined Tracing) probes integration
*/
public class JVMUSDTProbes {
/**
* JVM USDT probe definitions and management
*/
public static class JVMUSDTManager {
private final int targetPid;
private final Map<String, USDTProbe> activeProbes = new ConcurrentHashMap<>();
private final ExecutorService probeExecutor = Executors.newCachedThreadPool();
public JVMUSDTManager(int pid) {
this.targetPid = pid;
}
/**
* Enable common JVM USDT probes
*/
public void enableStandardProbes() {
enableMethodEntryProbe();
enableMethodReturnProbe();
enableMonitorProbes();
enableGCProbes();
enableMemoryProbes();
enableThreadProbes();
}
private void enableMethodEntryProbe() {
USDTProbe probe = new USDTProbe("hotspot", "method__entry", targetPid);
probe.setHandler(new MethodEntryHandler());
activeProbes.put("method_entry", probe);
probe.enable();
}
private void enableMethodReturnProbe() {
USDTProbe probe = new USDTProbe("hotspot", "method__return", targetPid);
probe.setHandler(new MethodReturnHandler());
activeProbes.put("method_return", probe);
probe.enable();
}
private void enableMonitorProbes() {
// Monitor contention probes
String[] monitorProbes = {
"monitor__wait", "monitor__waited", "monitor__contended__enter",
"monitor__contended__entered", "monitor__contended__exit"
};
for (String probeName : monitorProbes) {
USDTProbe probe = new USDTProbe("hotspot", probeName, targetPid);
probe.setHandler(new MonitorEventHandler());
activeProbes.put(probeName, probe);
probe.enable();
}
}
private void enableGCProbes() {
// Garbage Collection probes
String[] gcProbes = {
"gc__begin", "gc__end", "mem__pool__gc__begin", "mem__pool__gc__end"
};
for (String probeName : gcProbes) {
USDTProbe probe = new USDTProbe("hotspot", probeName, targetPid);
probe.setHandler(new GCEventHandler());
activeProbes.put(probeName, probe);
probe.enable();
}
}
private void enableMemoryProbes() {
// Memory allocation probes
String[] memoryProbes = {
"object__alloc"
};
for (String probeName : memoryProbes) {
USDTProbe probe = new USDTProbe("hotspot", probeName, targetPid);
probe.setHandler(new MemoryEventHandler());
activeProbes.put(probeName, probe);
probe.enable();
}
}
private void enableThreadProbes() {
// Thread lifecycle probes
String[] threadProbes = {
"thread__start", "thread__stop", "thread__park__begin",
"thread__park__end", "thread__unpark"
};
for (String probeName : threadProbes) {
USDTProbe probe = new USDTProbe("hotspot", probeName, targetPid);
probe.setHandler(new ThreadEventHandler());
activeProbes.put(probeName, probe);
probe.enable();
}
}
/**
* Disable all probes
*/
public void disableAllProbes() {
for (USDTProbe probe : activeProbes.values()) {
probe.disable();
}
activeProbes.clear();
}
/**
* Get probe statistics
*/
public Map<String, ProbeStatistics> getProbeStatistics() {
Map<String, ProbeStatistics> stats = new HashMap<>();
for (Map.Entry<String, USDTProbe> entry : activeProbes.entrySet()) {
stats.put(entry.getKey(), entry.getValue().getStatistics());
}
return stats;
}
}
/**
* USDT Probe representation
*/
public static class USDTProbe {
private final String provider;
private final String name;
private final int pid;
private volatile boolean enabled = false;
private USDTProbeHandler handler;
private final ProbeStatistics statistics = new ProbeStatistics();
public USDTProbe(String provider, String name, int pid) {
this.provider = provider;
this.name = name;
this.pid = pid;
}
public void enable() {
if (enabled) return;
try {
// Enable the USDT probe using systemtap or perf
String command = String.format(
"perf probe -x /proc/%d/exe %s:%s",
pid, provider, name
);
Process process = Runtime.getRuntime().exec(command);
int exitCode = process.waitFor();
if (exitCode == 0) {
enabled = true;
System.out.println("Enabled USDT probe: " + provider + ":" + name);
// Start listening for events
startEventListening();
} else {
System.err.println("Failed to enable probe: " + provider + ":" + name);
}
} catch (Exception e) {
System.err.println("Error enabling probe " + name + ": " + e.getMessage());
}
}
public void disable() {
if (!enabled) return;
try {
// Disable the probe
String command = String.format(
"perf probe --del %s:%s",
provider, name
);
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
enabled = false;
System.out.println("Disabled USDT probe: " + provider + ":" + name);
} catch (Exception e) {
System.err.println("Error disabling probe " + name + ": " + e.getMessage());
}
}
private void startEventListening() {
// In real implementation, this would read from perf events
// or use BPF to capture USDT events
}
public void setHandler(USDTProbeHandler handler) {
this.handler = handler;
}
public void handleEvent(USDTEvent event) {
statistics.recordEvent();
if (handler != null) {
handler.handleEvent(event);
}
}
public ProbeStatistics getStatistics() {
return statistics;
}
public String getFullName() {
return provider + ":" + name;
}
}
/**
* USDT Event data structure
*/
public static class USDTEvent {
private final String provider;
private final String probeName;
private final long timestamp;
private final int pid;
private final int tid;
private final Map<String, Object> arguments;
public USDTEvent(String provider, String probeName, long timestamp, 
int pid, int tid) {
this.provider = provider;
this.probeName = probeName;
this.timestamp = timestamp;
this.pid = pid;
this.tid = tid;
this.arguments = new HashMap<>();
}
public void addArgument(String name, Object value) {
arguments.put(name, value);
}
// Getters
public String getProvider() { return provider; }
public String getProbeName() { return probeName; }
public long getTimestamp() { return timestamp; }
public int getPid() { return pid; }
public int getTid() { return tid; }
public Map<String, Object> getArguments() { return arguments; }
@Override
public String toString() {
return String.format("USDTEvent[%s:%s, pid=%d, tid=%d, args=%s]",
provider, probeName, pid, tid, arguments);
}
}
/**
* Probe event handlers
*/
public interface USDTProbeHandler {
void handleEvent(USDTEvent event);
}
public static class MethodEntryHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
String className = (String) event.getArguments().get("class");
String methodName = (String) event.getArguments().get("method");
String signature = (String) event.getArguments().get("signature");
System.out.printf("METHOD ENTRY: %s.%s%s (tid: %d)%n",
className, methodName, signature, event.getTid());
}
}
public static class MethodReturnHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
// Handle method return event
}
}
public static class MonitorEventHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
String monitorType = event.getProbeName();
Object monitorAddress = event.getArguments().get("address");
System.out.printf("MONITOR %s: address=%s (tid: %d)%n",
monitorType.toUpperCase(), monitorAddress, event.getTid());
}
}
public static class GCEventHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
String gcName = (String) event.getArguments().get("name");
long used = (Long) event.getArguments().get("used");
long capacity = (Long) event.getArguments().get("capacity");
System.out.printf("GC %s: %s (used: %d, capacity: %d)%n",
event.getProbeName().toUpperCase(), gcName, used, capacity);
}
}
public static class MemoryEventHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
String className = (String) event.getArguments().get("class");
long size = (Long) event.getArguments().get("size");
long address = (Long) event.getArguments().get("address");
System.out.printf("ALLOCATION: %s (size: %d, address: 0x%x)%n",
className, size, address);
}
}
public static class ThreadEventHandler implements USDTProbeHandler {
@Override
public void handleEvent(USDTEvent event) {
String threadName = (String) event.getArguments().get("thread");
System.out.printf("THREAD %s: %s (tid: %d)%n",
event.getProbeName().toUpperCase(), threadName, event.getTid());
}
}
/**
* Probe statistics
*/
public static class ProbeStatistics {
private long eventCount = 0;
private long lastEventTime = 0;
private long totalEventSize = 0;
public void recordEvent() {
eventCount++;
lastEventTime = System.currentTimeMillis();
}
public void recordEvent(long eventSize) {
recordEvent();
totalEventSize += eventSize;
}
// Getters
public long getEventCount() { return eventCount; }
public long getLastEventTime() { return lastEventTime; }
public long getTotalEventSize() { return totalEventSize; }
public double getAverageEventSize() {
return eventCount > 0 ? (double) totalEventSize / eventCount : 0;
}
}
}

Performance Monitoring with eBPF

package com.example.ebpf;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Performance monitoring using eBPF
*/
public class EBPerformanceMonitor {
/**
* Comprehensive Java performance monitor using eBPF
*/
public static class JavaPerformanceMonitor {
private final int targetPid;
private final Map<String, PerformanceMetric> metrics = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
private volatile boolean monitoring = false;
public JavaPerformanceMonitor(int pid) {
this.targetPid = pid;
initializeMetrics();
}
private void initializeMetrics() {
// Method execution metrics
metrics.put("method_calls", new PerformanceMetric("method_calls", "count"));
metrics.put("method_duration", new PerformanceMetric("method_duration", "nanoseconds"));
// Memory metrics
metrics.put("allocations", new PerformanceMetric("allocations", "count"));
metrics.put("allocation_size", new PerformanceMetric("allocation_size", "bytes"));
// GC metrics
metrics.put("gc_count", new PerformanceMetric("gc_count", "count"));
metrics.put("gc_pause_time", new PerformanceMetric("gc_pause_time", "nanoseconds"));
// Thread metrics
metrics.put("thread_creations", new PerformanceMetric("thread_creations", "count"));
metrics.put("thread_context_switches", new PerformanceMetric("thread_context_switches", "count"));
// System metrics
metrics.put("cpu_usage", new PerformanceMetric("cpu_usage", "percentage"));
metrics.put("memory_usage", new PerformanceMetric("memory_usage", "bytes"));
}
public void startMonitoring() {
if (monitoring) return;
monitoring = true;
// Start eBPF-based monitoring
startEBPFMonitoring();
// Start periodic metric collection
scheduler.scheduleAtFixedRate(this::collectSystemMetrics, 1, 1, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::reportMetrics, 5, 5, TimeUnit.SECONDS);
System.out.println("Started performance monitoring for PID: " + targetPid);
}
public void stopMonitoring() {
if (!monitoring) return;
monitoring = false;
scheduler.shutdown();
System.out.println("Stopped performance monitoring");
}
private void startEBPFMonitoring() {
// In real implementation, this would load eBPF programs
// for performance monitoring
// Simulate eBPF event processing
scheduler.scheduleAtFixedRate(() -> {
// Simulate method call events
metrics.get("method_calls").addValue(1);
metrics.get("method_duration").addValue(ThreadLocalRandom.current().nextLong(1000, 100000));
// Simulate allocation events
if (ThreadLocalRandom.current().nextDouble() < 0.3) {
metrics.get("allocations").addValue(1);
metrics.get("allocation_size").addValue(ThreadLocalRandom.current().nextLong(100, 10000));
}
// Simulate GC events
if (ThreadLocalRandom.current().nextDouble() < 0.01) {
metrics.get("gc_count").addValue(1);
metrics.get("gc_pause_time").addValue(ThreadLocalRandom.current().nextLong(1000000, 10000000));
}
}, 0, 100, TimeUnit.MILLISECONDS);
}
private void collectSystemMetrics() {
try {
// Collect CPU usage from /proc
double cpuUsage = collectCPUUsage();
metrics.get("cpu_usage").addValue((long)(cpuUsage * 100));
// Collect memory usage
long memoryUsage = collectMemoryUsage();
metrics.get("memory_usage").addValue(memoryUsage);
} catch (Exception e) {
System.err.println("Error collecting system metrics: " + e.getMessage());
}
}
private double collectCPUUsage() {
try {
// Read /proc/stat for system CPU usage
// This is simplified - real implementation would be more accurate
return ThreadLocalRandom.current().nextDouble(0.1, 0.8);
} catch (Exception e) {
return 0.0;
}
}
private long collectMemoryUsage() {
try {
// Read /proc/pid/statm for process memory usage
Path statmPath = Paths.get("/proc", String.valueOf(targetPid), "statm");
if (Files.exists(statmPath)) {
String content = Files.readString(statmPath);
String[] parts = content.split("\\s+");
if (parts.length > 1) {
long pages = Long.parseLong(parts[1]);
return pages * 4096; // Convert pages to bytes
}
}
} catch (Exception e) {
// Ignore errors
}
return 0;
}
private void reportMetrics() {
if (!monitoring) return;
System.out.println("\n=== Performance Metrics Report ===");
for (PerformanceMetric metric : metrics.values()) {
if (metric.getCount() > 0) {
System.out.printf("  %s: %s%n", metric.getName(), metric.getFormattedValue());
}
}
System.out.println("==================================\n");
}
public PerformanceMetric getMetric(String name) {
return metrics.get(name);
}
public Map<String, PerformanceMetric> getAllMetrics() {
return new HashMap<>(metrics);
}
public PerformanceReport generateReport() {
return new PerformanceReport(new HashMap<>(metrics));
}
}
/**
* Performance metric representation
*/
public static class PerformanceMetric {
private final String name;
private final String unit;
private final AtomicLong count = new AtomicLong();
private final AtomicLong sum = new AtomicLong();
private final AtomicLong min = new AtomicLong(Long.MAX_VALUE);
private final AtomicLong max = new AtomicLong(Long.MIN_VALUE);
public PerformanceMetric(String name, String unit) {
this.name = name;
this.unit = unit;
}
public void addValue(long value) {
count.incrementAndGet();
sum.addAndGet(value);
// Update min
long currentMin;
do {
currentMin = min.get();
} while (value < currentMin && !min.compareAndSet(currentMin, value));
// Update max
long currentMax;
do {
currentMax = max.get();
} while (value > currentMax && !max.compareAndSet(currentMax, value));
}
public void reset() {
count.set(0);
sum.set(0);
min.set(Long.MAX_VALUE);
max.set(Long.MIN_VALUE);
}
// Getters
public String getName() { return name; }
public String getUnit() { return unit; }
public long getCount() { return count.get(); }
public long getSum() { return sum.get(); }
public long getMin() { 
long minVal = min.get();
return minVal == Long.MAX_VALUE ? 0 : minVal;
}
public long getMax() { 
long maxVal = max.get();
return maxVal == Long.MIN_VALUE ? 0 : maxVal;
}
public double getAverage() {
return count.get() > 0 ? (double) sum.get() / count.get() : 0.0;
}
public String getFormattedValue() {
switch (unit) {
case "count":
return String.format("%,d", getCount());
case "nanoseconds":
return String.format("avg: %.2f ms, min: %.2f ms, max: %.2f ms",
getAverage() / 1_000_000.0, getMin() / 1_000_000.0, getMax() / 1_000_000.0);
case "bytes":
return String.format("total: %s, avg: %s",
formatBytes(getSum()), formatBytes((long)getAverage()));
case "percentage":
return String.format("%.1f%%", getAverage());
default:
return String.format("%,d %s", (long)getAverage(), unit);
}
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
}
}
/**
* Comprehensive performance report
*/
public static class PerformanceReport {
private final Map<String, PerformanceMetric> metrics;
private final long timestamp;
public PerformanceReport(Map<String, PerformanceMetric> metrics) {
this.metrics = metrics;
this.timestamp = System.currentTimeMillis();
}
public void printReport() {
System.out.println("\n" + "=".repeat(60));
System.out.println("           JAVA PERFORMANCE REPORT");
System.out.println("=".repeat(60));
System.out.printf("Generated at: %tT%n", timestamp);
printSection("Method Execution", "method_");
printSection("Memory", "allocations", "allocation_size", "memory_usage");
printSection("Garbage Collection", "gc_");
printSection("Threads", "thread_");
printSection("System", "cpu_usage");
System.out.println("=".repeat(60));
}
private void printSection(String title, String... metricPrefixes) {
System.out.println("\n" + title.toUpperCase());
System.out.println("-".repeat(title.length()));
for (String prefix : metricPrefixes) {
for (Map.Entry<String, PerformanceMetric> entry : metrics.entrySet()) {
if (entry.getKey().startsWith(prefix) && entry.getValue().getCount() > 0) {
System.out.printf("  %-25s: %s%n", 
entry.getKey(), entry.getValue().getFormattedValue());
}
}
}
}
public Map<String, Object> toMap() {
Map<String, Object> report = new HashMap<>();
report.put("timestamp", timestamp);
Map<String, Object> metricData = new HashMap<>();
for (Map.Entry<String, PerformanceMetric> entry : metrics.entrySet()) {
PerformanceMetric metric = entry.getValue();
Map<String, Object> metricInfo = new HashMap<>();
metricInfo.put("count", metric.getCount());
metricInfo.put("sum", metric.getSum());
metricInfo.put("min", metric.getMin());
metricInfo.put("max", metric.getMax());
metricInfo.put("average", metric.getAverage());
metricInfo.put("unit", metric.getUnit());
metricData.put(entry.getKey(), metricInfo);
}
report.put("metrics", metricData);
return report;
}
}
}

Real-time Event Processing

package com.example.ebpf;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Real-time event processing from eBPF
*/
public class RealTimeEventProcessor {
/**
* Processes eBPF events in real-time
*/
public static class EBPEventProcessor {
private final BlockingQueue<BPFEvent> eventQueue = new LinkedBlockingQueue<>();
private final Map<String, EventHandler> eventHandlers = new ConcurrentHashMap<>();
private final ExecutorService processorExecutor = Executors.newFixedThreadPool(4);
private final AtomicLong processedEvents = new AtomicLong();
private volatile boolean processing = false;
public EBPEventProcessor() {
registerDefaultHandlers();
}
private void registerDefaultHandlers() {
// Method execution events
eventHandlers.put("method_entry", new MethodEntryEventHandler());
eventHandlers.put("method_exit", new MethodExitEventHandler());
// Memory events
eventHandlers.put("memory_allocation", new MemoryAllocationEventHandler());
eventHandlers.put("gc_event", new GCEventHandler());
// Thread events
eventHandlers.put("thread_start", new ThreadStartEventHandler());
eventHandlers.put("thread_end", new ThreadEndEventHandler());
// System events
eventHandlers.put("system_call", new SystemCallEventHandler());
}
public void startProcessing() {
if (processing) return;
processing = true;
// Start event processing threads
for (int i = 0; i < 4; i++) {
processorExecutor.submit(this::processEvents);
}
System.out.println("Started eBPF event processing");
}
public void stopProcessing() {
processing = false;
processorExecutor.shutdown();
try {
if (!processorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
processorExecutor.shutdownNow();
}
} catch (InterruptedException e) {
processorExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("Stopped eBPF event processing. Processed " + 
processedEvents.get() + " events.");
}
public void submitEvent(BPFEvent event) {
if (processing) {
eventQueue.offer(event);
}
}
private void processEvents() {
while (processing || !eventQueue.isEmpty()) {
try {
BPFEvent event = eventQueue.poll(100, TimeUnit.MILLISECONDS);
if (event != null) {
processSingleEvent(event);
processedEvents.incrementAndGet();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
System.err.println("Error processing event: " + e.getMessage());
}
}
}
private void processSingleEvent(BPFEvent event) {
EventHandler handler = eventHandlers.get(event.getType());
if (handler != null) {
handler.handle(event);
} else {
// Default handler for unknown event types
System.out.println("Unhandled event type: " + event.getType() + " - " + event);
}
}
public void registerHandler(String eventType, EventHandler handler) {
eventHandlers.put(eventType, handler);
}
public long getProcessedEventCount() {
return processedEvents.get();
}
public int getQueuedEventCount() {
return eventQueue.size();
}
}
/**
* eBPF event representation
*/
public static class BPFEvent {
private final String type;
private final long timestamp;
private final int pid;
private final int tid;
private final Map<String, Object> data;
public BPFEvent(String type, long timestamp, int pid, int tid) {
this.type = type;
this.timestamp = timestamp;
this.pid = pid;
this.tid = tid;
this.data = new HashMap<>();
}
public void setData(String key, Object value) {
data.put(key, value);
}
public Object getData(String key) {
return data.get(key);
}
// Getters
public String getType() { return type; }
public long getTimestamp() { return timestamp; }
public int getPid() { return pid; }
public int getTid() { return tid; }
public Map<String, Object> getData() { return data; }
@Override
public String toString() {
return String.format("BPFEvent[type=%s, pid=%d, tid=%d, data=%s]",
type, pid, tid, data);
}
}
/**
* Event handler interface
*/
public interface EventHandler {
void handle(BPFEvent event);
}
/**
* Method entry event handler
*/
public static class MethodEntryEventHandler implements EventHandler {
private final Map<String, AtomicLong> methodCallCounts = new ConcurrentHashMap<>();
@Override
public void handle(BPFEvent event) {
String className = (String) event.getData().get("class_name");
String methodName = (String) event.getData().get("method_name");
String fullMethodName = className + "." + methodName;
// Track method call frequency
methodCallCounts.computeIfAbsent(fullMethodName, k -> new AtomicLong())
.incrementAndGet();
// Real-time analysis could go here
if (isHotMethod(fullMethodName)) {
System.out.printf("HOT METHOD: %s (calls: %d)%n", 
fullMethodName, methodCallCounts.get(fullMethodName).get());
}
}
private boolean isHotMethod(String methodName) {
AtomicLong count = methodCallCounts.get(methodName);
return count != null && count.get() > 1000;
}
public Map<String, Long> getMethodCallCounts() {
Map<String, Long> counts = new HashMap<>();
for (Map.Entry<String, AtomicLong> entry : methodCallCounts.entrySet()) {
counts.put(entry.getKey(), entry.getValue().get());
}
return counts;
}
}
/**
* Method exit event handler with timing
*/
public static class MethodExitEventHandler implements EventHandler {
private final Map<Long, Long> methodStartTimes = new ConcurrentHashMap<>();
private final Map<String, MethodTimingStats> methodTimings = new ConcurrentHashMap<>();
@Override
public void handle(BPFEvent event) {
Long startTime = methodStartTimes.remove(event.getTid());
if (startTime != null) {
long duration = event.getTimestamp() - startTime;
String methodName = (String) event.getData().get("method_name");
MethodTimingStats stats = methodTimings.computeIfAbsent(methodName,
k -> new MethodTimingStats(methodName));
stats.recordTiming(duration);
// Alert on slow methods
if (duration > 10_000_000L) { // 10ms threshold
System.out.printf("SLOW METHOD: %s took %.2f ms%n",
methodName, duration / 1_000_000.0);
}
}
}
public void recordMethodEntry(int tid, long timestamp) {
methodStartTimes.put((long)tid, timestamp);
}
public Map<String, MethodTimingStats> getMethodTimings() {
return new HashMap<>(methodTimings);
}
}
/**
* Memory allocation event handler
*/
public static class MemoryAllocationEventHandler implements EventHandler {
private final AtomicLong totalAllocations = new AtomicLong();
private final AtomicLong totalAllocatedBytes = new AtomicLong();
private final Map<String, AtomicLong> allocationByClass = new ConcurrentHashMap<>();
@Override
public void handle(BPFEvent event) {
long size = (Long) event.getData().get("size");
String className = (String) event.getData().get("class_name");
totalAllocations.incrementAndGet();
totalAllocatedBytes.addAndGet(size);
allocationByClass.computeIfAbsent(className, k -> new AtomicLong())
.addAndGet(size);
// Detect memory allocation patterns
if (totalAllocations.get() % 1000 == 0) {
System.out.printf("Memory allocations: %,d (%,d bytes)%n",
totalAllocations.get(), totalAllocatedBytes.get());
}
}
public Map<String, Long> getAllocationByClass() {
Map<String, Long> result = new HashMap<>();
for (Map.Entry<String, AtomicLong> entry : allocationByClass.entrySet()) {
result.put(entry.getKey(), entry.getValue().get());
}
return result;
}
}
/**
* GC event handler
*/
public static class GCEventHandler implements EventHandler {
private final AtomicLong gcCount = new AtomicLong();
private final AtomicLong totalGCTime = new AtomicLong();
@Override
public void handle(BPFEvent event) {
gcCount.incrementAndGet();
long pauseTime = (Long) event.getData().get("pause_time");
totalGCTime.addAndGet(pauseTime);
System.out.printf("GC Event: %s, pause: %.2f ms%n",
event.getData().get("gc_type"), pauseTime / 1_000_000.0);
}
public long getGCCount() {
return gcCount.get();
}
public double getAverageGCTime() {
return gcCount.get() > 0 ? (double) totalGCTime.get() / gcCount.get() : 0.0;
}
}
/**
* Thread event handlers
*/
public static class ThreadStartEventHandler implements EventHandler {
private final AtomicLong threadStartCount = new AtomicLong();
@Override
public void handle(BPFEvent event) {
threadStartCount.incrementAndGet();
String threadName = (String) event.getData().get("thread_name");
System.out.printf("Thread started: %s (total: %d)%n",
threadName, threadStartCount.get());
}
}
public static class ThreadEndEventHandler implements EventHandler {
@Override
public void handle(BPFEvent event) {
String threadName = (String) event.getData().get("thread_name");
System.out.println("Thread ended: " + threadName);
}
}
/**
* System call event handler
*/
public static class SystemCallEventHandler implements EventHandler {
private final Map<Integer, AtomicLong> syscallCounts = new ConcurrentHashMap<>();
@Override
public void handle(BPFEvent event) {
int syscallNumber = (Integer) event.getData().get("syscall_number");
syscallCounts.computeIfAbsent(syscallNumber, k -> new AtomicLong())
.incrementAndGet();
}
public Map<Integer, Long> getSyscallCounts() {
Map<Integer, Long> counts = new HashMap<>();
for (Map.Entry<Integer, AtomicLong> entry : syscallCounts.entrySet()) {
counts.put(entry.getKey(), entry.getValue().get());
}
return counts;
}
}
/**
* Method timing statistics
*/
public static class MethodTimingStats {
private final String methodName;
private final AtomicLong callCount = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
private volatile long minTime = Long.MAX_VALUE;
private volatile long maxTime = Long.MIN_VALUE;
public MethodTimingStats(String methodName) {
this.methodName = methodName;
}
public void recordTiming(long duration) {
callCount.incrementAndGet();
totalTime.addAndGet(duration);
if (duration < minTime) minTime = duration;
if (duration > maxTime) maxTime = duration;
}
// Getters
public String getMethodName() { return methodName; }
public long getCallCount() { return callCount.get(); }
public double getAverageTime() {
return callCount.get() > 0 ? (double) totalTime.get() / callCount.get() : 0.0;
}
public long getMinTime() { return minTime == Long.MAX_VALUE ? 0 : minTime; }
public long getMaxTime() { return maxTime == Long.MIN_VALUE ? 0 : maxTime; }
}
}

Complete Usage Example

package com.example.ebpf;
import java.util.concurrent.TimeUnit;
/**
* Complete eBPF tracing example for Java processes
*/
public class EBPTracingExample {
public static void main(String[] args) throws Exception {
// Find a Java process to monitor
int javaPid = findJavaProcess();
if (javaPid == -1) {
System.out.println("No Java process found to monitor");
return;
}
System.out.println("Monitoring Java process with PID: " + javaPid);
// Create and configure the eBPF tracer
JavaEBPFTracer.JavaProcessTracer tracer = 
new JavaEBPFTracer.JavaProcessTracer(javaPid);
// Setup USDT probes
JVMUSDTProbes.JVMUSDTManager usdtManager = 
new JVMUSDTProbes.JVMUSDTManager(javaPid);
// Setup performance monitoring
EBPerformanceMonitor.JavaPerformanceMonitor perfMonitor = 
new EBPerformanceMonitor.JavaPerformanceMonitor(javaPid);
// Setup real-time event processing
RealTimeEventProcessor.EBPEventProcessor eventProcessor = 
new RealTimeEventProcessor.EBPEventProcessor();
try {
// Start all monitoring components
System.out.println("Starting eBPF tracing...");
tracer.startTracing();
usdtManager.enableStandardProbes();
perfMonitor.startMonitoring();
eventProcessor.startProcessing();
// Monitor for a period
System.out.println("Monitoring for 30 seconds...");
TimeUnit.SECONDS.sleep(30);
// Generate reports
System.out.println("\nGenerating performance report...");
EBPerformanceMonitor.PerformanceReport report = perfMonitor.generateReport();
report.printReport();
// Show probe statistics
System.out.println("\nProbe Statistics:");
usdtManager.getProbeStatistics().forEach((name, stats) -> {
System.out.printf("  %s: %,d events%n", name, stats.getEventCount());
});
} finally {
// Clean up
System.out.println("\nCleaning up...");
eventProcessor.stopProcessing();
perfMonitor.stopMonitoring();
usdtManager.disableAllProbes();
tracer.stopTracing();
}
}
private static int findJavaProcess() {
try {
// Look for a Java process
Process process = Runtime.getRuntime().exec(new String[]{
"pgrep", "-f", "java"
});
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
String line = reader.readLine();
if (line != null) {
return Integer.parseInt(line.trim());
}
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
/**
* Example of a Java application that can be monitored
*/
public static class MonitorableApplication {
public static void main(String[] args) throws Exception {
System.out.println("Monitorable Java Application Started");
// Simulate various activities that can be traced
while (true) {
performBusinessLogic();
allocateMemory();
triggerGC();
manageThreads();
Thread.sleep(1000);
}
}
private static void performBusinessLogic() {
// Method calls that will be traced
calculateSum(1000);
processData("sample data");
validateInput(42);
}
private static long calculateSum(int n) {
long sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
private static String processData(String data) {
return data.toUpperCase() + "_processed";
}
private static boolean validateInput(int input) {
return input > 0 && input < 1000;
}
private static void allocateMemory() {
// Trigger some memory allocations
byte[] buffer = new byte[1024 * 1024]; // 1MB allocation
String[] strings = new String[1000];
for (int i = 0; i < strings.length; i++) {
strings[i] = "string_" + i;
}
}
private static void triggerGC() {
// Suggest GC (may or may not run)
if (Math.random() < 0.1) {
System.gc();
}
}
private static void manageThreads() {
// Create some short-lived threads
if (Math.random() < 0.2) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}
}

Best Practices for eBPF Java Tracing

  1. Security Considerations:
  • Run with appropriate privileges (root or CAP_BPF)
  • Validate all eBPF programs before loading
  • Use bounded loops and limited complexity in eBPF code
  1. Performance Impact:
  • Limit the number of active probes
  • Use appropriate sampling rates for high-frequency events
  • Monitor system resource usage during tracing
  1. Error Handling:
  • Implement graceful degradation when eBPF features are unavailable
  • Handle BPF program verification failures
  • Manage resource limits (memory, CPU)
  1. Production Readiness:
  • Test thoroughly in staging environments
  • Implement proper log rotation and monitoring
  • Consider overhead on production systems

Conclusion

eBPF provides powerful capabilities for tracing Java processes with minimal overhead. Key benefits include:

  • Low Overhead: In-kernel execution avoids context switches
  • Rich Observability: Access to system calls, network, and application events
  • Safety: Verifiable programs prevent kernel crashes
  • Flexibility: Dynamic program loading and attachment

By combining eBPF with JVM USDT probes and performance monitoring, you can achieve comprehensive observability of Java applications in production environments. The techniques shown provide a foundation for building sophisticated monitoring and debugging tools that leverage modern Linux kernel capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper