Debugging file I/O in Java at the call-stack level means linking every native write() syscall back to the Java business method that triggered it — something no IDE breakpoint reaches and no coredump preserves. This post shows how with time-travel replay: record a running Java process, break on write(), and reconstruct the full Java stack from JNI down to your Files.write caller.

Where IDE Breakpoints and Coredumps Fall Short in Java File I/O

Two tools most Java developers reach for first can’t answer the question “which business method triggered this file write”:

  • IDE breakpoints stop at the JVM boundary. They can pause at Java methods, but they can’t break on the underlying write() syscall inside libc — the exact point where a file write actually leaves your program.

  • Native stack traces stop at the JNI layer. Attaching a C-level debugger and running bt on a file write reaches Java_sun_nio_ch_FileDispatcherImpl_write0 and loses the trail — nothing about which Java method triggered it.

  • Coredumps are single-moment snapshots. A coredump captures state at one point in time and cannot recreate the execution path leading up to it, so you can’t step back to see prior writes or what the Java stack looked like at each one.

Time-travel replay closes both gaps: it records the whole run so you can navigate the timeline, and it lets you break on native syscalls with a full Java stack reconstructable at every stop.

Reconstructing the Full Java Call Stack from a Native write() Syscall

The reconstruction relies on two pieces:

  1. UDB records the entire execution of the JVM process, so you can pause on any syscall — including write() inside libc — and step forward or backward through the timeline.

  2. OpenResty XRay ships a Y-language script java-udb.y.py that, once loaded inside UDB, adds a java_bt command. java_bt prints the complete Java call stack at any breakpoint — from C:__libc_write up through the JNI layer to your business method — turning the C-only backtrace you get from bt into a full end-to-end path.

The rest of this post walks through recording, breakpointing, and reconstructing on a real Java process.

Debug Java File I/O Step by Step with Time-Travel Replay

The five steps below record a running Java process, break on the native write() syscall, and reconstruct the Java call stack behind each write:

Step 1: Recording Application Execution Traces

First, we’ll use UDB’s Live Record tool to capture the execution process of a Java application:

  1. Utilize the Live Record tool to record a sample of a running Java application.

  2. Load the recorded sample with the UDB tool and set up the debugging environment:

udb -ex "set pagination off" -ex "set python print-stack full" java.rec

Step 2: Locating File Operation Breakpoints

In the UDB environment, set breakpoints to capture file write operations:

start 1> break write
start 1> c
Continuing.
[Switching to Thread 3923049.3923068]

Thread 20 "Thread-0" hit Breakpoint 1.768, 0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6

Step 3: Analyzing the Underlying C Call Stack

Use the bt command to examine the current C-level call stack:

4% 7,955> bt
#0  0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6
#1  0x00007ffff7e32c5f in Java_sun_nio_ch_FileDispatcherImpl_write0 (env=0x7ffff0181290, clazz=<optimized out>, fdo=<optimized out>, address=140733730265536, len=81)
    at src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c:118
#2  0x00007fffe0baacf6 in ?? ()
#3  0x000000062a39ed70 in ?? ()

From the C call stack, we can see that the system’s low-level write system call has been triggered. However, this only shows information at the JNI layer and doesn’t provide visibility into the complete call path of the Java business code.

Step 4: Analyzing the Complete Java Call Stack

  1. Load the Java call stack analysis tool provided by OpenResty XRay:
4% 7,955> source java-udb.y.py
  1. Use the java_bt command to obtain the complete Java call stack:
4% 7,955> java_bt
Start tracing...
C:__libc_write
sun.nio.ch.FileDispatcherImpl:write0(Native Method)
@FileDispatcherImpl.java:62
sun.nio.ch.FileDispatcherImpl:write
@IOUtil.java:114
sun.nio.ch.IOUtil:writeFromNativeBuffer
@IOUtil.java:75
sun.nio.ch.IOUtil:write
@IOUtil.java:67
sun.nio.ch.IOUtil:write
@FileChannelImpl.java:288
sun.nio.ch.FileChannelImpl:write
@Channels.java:89
java.nio.channels.Channels:writeFully
@Channels.java:158
java.nio.channels.Channels$1:write
@StreamEncoder.java:223
sun.nio.cs.StreamEncoder:writeBytes
@StreamEncoder.java:325
sun.nio.cs.StreamEncoder:implClose
@StreamEncoder.java:165
sun.nio.cs.StreamEncoder:close
@OutputStreamWriter.java:252
java.io.OutputStreamWriter:close
@BufferedWriter.java:263
java.io.BufferedWriter:close
@Files.java:3579
java.nio.file.Files:write
@Processor.java:42
FileWriterTask:run
@Thread.java:840
java.lang.Thread:run

Through this complete Java call stack, we can clearly visualize the entire path from the business code FileWriterTask:run method, through Java’s standard library Files.write method, all the way down to the underlying system calls. This visibility provides critical value for understanding application file operation behaviors, identifying performance bottlenecks, or troubleshooting file-related issues. Once you’ve identified the responsible business method, OpenResty XRay can also capture the arguments passed to that method live, without a Java agent or bytecode instrumentation.

Comparing File Write Call Stacks Across Time

Time-travel debugging capability in UDB represents one of its most powerful features. This functionality allows developers to freely move forward or backward through recorded execution traces, precisely pinpointing different file operation timestamps, and comprehensively analyzing the complete context and call stack for each write operation. This capability becomes particularly crucial when troubleshooting complex file operation issues.

To demonstrate this capability, we can continue executing the program to capture the next file write operation:

8% 16,337> c
Continuing.

Thread 20 "Thread-0" hit Breakpoint 1.768, 0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6

8% 16,337> java_bt
Start tracing...
C:__libc_write
sun.nio.ch.FileDispatcherImpl:write0(Native Method)
@FileDispatcherImpl.java:62
sun.nio.ch.FileDispatcherImpl:write
@IOUtil.java:114
sun.nio.ch.IOUtil:writeFromNativeBuffer
@IOUtil.java:75
sun.nio.ch.IOUtil:write
@IOUtil.java:67
sun.nio.ch.IOUtil:write
@FileChannelImpl.java:288
sun.nio.ch.FileChannelImpl:write
@Channels.java:89
java.nio.channels.Channels:writeFully
@Channels.java:158
java.nio.channels.Channels$1:write
@StreamEncoder.java:223
sun.nio.cs.StreamEncoder:writeBytes
@StreamEncoder.java:325
sun.nio.cs.StreamEncoder:implClose
@StreamEncoder.java:165
sun.nio.cs.StreamEncoder:close
@OutputStreamWriter.java:252
java.io.OutputStreamWriter:close
@BufferedWriter.java:263
java.io.BufferedWriter:close
@Files.java:3579
java.nio.file.Files:write
@Processor.java:203
CoreWriterTask:run
@Thread.java:840
java.lang.Thread:run

By comparing two captured call stacks, we can clearly see:

  1. The first write operation originates from the FileWriterTask:run method (located at Processor.java:42)
  2. The second write operation comes from the CoreWriterTask:run method (located at Processor.java:203)

This analytical approach isn’t limited to file write operations — it’s equally applicable to file reads, network communications, and other I/O paths. For a broader profiling angle, OpenResty XRay can also analyze CPU, off-CPU, and disk I/O usage of Java applications without JVM safepoints.

The dynamic analysis capabilities of OpenResty XRay combined with UDB enable us to gain complete visibility into file operation patterns within applications, breaking through the limitations of traditional debugging methods that only capture state at a single moment. Particularly in scenarios where processes have terminated or no longer exist, UDB’s retrospective analysis capabilities demonstrate unique advantages over traditional GDB coredump analysis.

A coredump only provides a static snapshot at the moment of a program crash, unable to recreate the complete execution path leading up to the failure. In contrast, UDB’s recording functionality empowers OpenResty XRay to navigate freely through the entire program execution history, even when the original process is no longer running. This allows for precise examination of program state and behavioral details at any point in time, truly achieving comprehensive debugging across the time dimension.

Frequently Asked Questions

How do I see the full call stack of a Java file write?

Break on the native write() syscall inside a recorded JVM run (UDB’s break write), then run java_bt — a command added by loading OpenResty XRay’s Y-language script with source java-udb.y.py. It prints the complete Java stack, from C:__libc_write at the bottom to the business method that triggered the write at the top.

How is time-travel debugging different from analyzing a coredump?

A coredump is a single-moment snapshot at the point of crash — you can inspect state but cannot replay how the program got there. Time-travel debugging records the entire execution, so you can step backward and forward, pause on any syscall, and reconstruct the call stack at every past moment during the run.

Why does the C-level bt only show JNI, not the Java business method?

The bt command in a native debugger walks C stack frames. When Java code calls a file write, the C-level trace reaches Java_sun_nio_ch_FileDispatcherImpl_write0 — the JNI entry point — and stops there, because the JVM’s interpreter and JIT frames are not standard C frames. You need a JVM-aware tool to unwind them; OpenResty XRay’s java_bt command does this.

Conclusion

The Java call stack analysis capabilities provided by UDB in conjunction with OpenResty XRay offer developers unprecedented insight into application behavior. Through time-travel debugging and comprehensive call stack analysis, developers can:

  1. Precisely pinpoint the source and context of file operations
  2. Perform deep analysis of I/O performance bottlenecks
  3. Efficiently troubleshoot file operation-related issues
  4. Thoroughly validate the security and correctness of file operations

This depth of analysis is particularly crucial for Java application development, especially in today’s microservices-dominated landscape, where such deep analytical capabilities can be a genuine lifesaver.

We hope this practical demonstration helps you better understand and appreciate the use cases and power of UDB and OpenResty XRay. If you’re struggling with complex debugging challenges, this powerful combination might be exactly what you need.

What is OpenResty XRay

OpenResty XRay is a dynamic-tracing product that automatically analyzes your running applications to troubleshoot performance problems, behavioral issues, and security vulnerabilities with actionable suggestions. Under the hood, OpenResty XRay is powered by our Y language targeting various runtimes like Stap+, eBPF+, GDB, and ODB, depending on the contexts.

If you like this tutorial, please subscribe to this blog site and/or our YouTube channel. Thank you!

About The Author

Yichun Zhang (Github handle: agentzh), is the original creator of the OpenResty® open-source project and the CEO of OpenResty Inc..

Yichun is one of the earliest advocates and leaders of “open-source technology”. He worked at many internationally renowned tech companies, such as Cloudflare, Yahoo!. He is a pioneer of “edge computing”, “dynamic tracing” and “machine coding”, with over 22 years of programming and 16 years of open source experience. Yichun is well-known in the open-source space as the project leader of OpenResty®, adopted by more than 40 million global website domains.

OpenResty Inc., the enterprise software start-up founded by Yichun in 2017, has customers from some of the biggest companies in the world. Its flagship product, OpenResty XRay, is a non-invasive profiling and troubleshooting tool that significantly enhances and utilizes dynamic tracing technology. And its OpenResty Edge product is a powerful distributed traffic management and private CDN software product.

As an avid open-source contributor, Yichun has contributed more than a million lines of code to numerous open-source projects, including Linux kernel, Nginx, LuaJIT, GDB, SystemTap, LLVM, Perl, etc. He has also authored more than 60 open-source software libraries.