How to Debug File I/O in Java with Time-Travel Call Stacks
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
bton a file write reachesJava_sun_nio_ch_FileDispatcherImpl_write0and 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:
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.OpenResty XRay ships a Y-language script
java-udb.y.pythat, once loaded inside UDB, adds ajava_btcommand.java_btprints the complete Java call stack at any breakpoint — fromC:__libc_writeup through the JNI layer to your business method — turning the C-only backtrace you get frombtinto 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:
Utilize the Live Record tool to record a sample of a running Java application.
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
- Load the Java call stack analysis tool provided by OpenResty XRay:
4% 7,955> source java-udb.y.py
- Use the
java_btcommand 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:
- The first write operation originates from the
FileWriterTask:runmethod (located atProcessor.java:42) - The second write operation comes from the
CoreWriterTask:runmethod (located atProcessor.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:
- Precisely pinpoint the source and context of file operations
- Perform deep analysis of I/O performance bottlenecks
- Efficiently troubleshoot file operation-related issues
- 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.

















