nio2: NIO.2 file API companion code (streaming 5 GB at -Xmx64m, handle leaks, WatchService, mmap)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
2026-09-30 19:14:31 +00:00
co-authored by Claude Sonnet 5.5
parent f4b732799d
commit 1ac2f6077a
30 changed files with 1072 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@ article; each module's own README has that article's version table, quickstart,
| [`serialization`](serialization/) | Java Serialization in 2026: Why It's Dangerous and What Replaced It |
| [`strings`](strings/) | Java Strings Deep Dive: Interning, StringBuilder, Text Blocks, and the Concatenation Benchmark |
| [`collections`](collections/) | How to Sort a HashMap by Value (and Key) in Java |
| [`nio2`](nio2/) | Java NIO.2 File API: Files, Path, WatchService, and Streaming Large Files Without OOM |
## License
+44
View File
@@ -0,0 +1,44 @@
# nio2
Companion code for the ankurm.com post *"Java NIO.2 File API: Files, Path, WatchService, and Streaming
Large Files Without OOM."* Module `nio2` in `java-core-examples`.
All explanation lives in the post; this module holds the runnable evidence and the captured output.
## Versions
| Component | Version |
|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) |
| JUnit Jupiter | 5.11.0 |
| OS | Linux 6.18 (WatchService and `/proc/self/fd` results are Linux-specific) |
| Hardware | 2 vCPU x86-64 VM shared with other jobs (timings are indicative, not a leaderboard) |
| Big file | 5,000,000,069 bytes (the full 5 GB fit; the file is generated, then deleted by the script) |
## Quickstart
```bash
export JDK25_HOME=/path/to/jdk-25
./scripts/run-all.sh # needs ~5.3 GB free in /tmp/nio2-data; regenerates output/
BIG_BYTES=500000000 ./scripts/run-all.sh # smaller "big" file if disk is tight
mvn -q -pl nio2 -am test # 12 assertions, small files only
```
## What is in here
| File | Shows | Output |
|---|---|---|
| `LogFile` | deterministic generator for the synthetic log file | `02` |
| `StreamLines` | `Files.lines` and `Files.newBufferedReader` over 5 GB at `-Xmx64m`, heap and GC evidence | `03`, `04` |
| `ReadAllFail` | `readAllLines` / `readString` / `readAllBytes` at the same `-Xmx`, and how much heap they need | `05` |
| `HandleLeakDemo` | open file descriptors around unclosed `Files.lines` and `Files.walk` | `06` |
| `HandleExhaustion` | what an unclosed `Files.walk` becomes under `ulimit -n 64` | `07` |
| `WalkFindDemo` | `list` / `walk` / `find` / `walkFileTree` / `DirectoryStream`, symlink loop | `08` |
| `LinesCharsetDemo` | a bad byte fails lazily inside `Files.lines` | `09` |
| `WatchDemo` | WatchService events, coalescing, no recursion, OVERFLOW at 513 pending events | `10`, `11` |
| (JDK source excerpt) | `AbstractWatchKey.signalEvent` from the JDK's own `src.zip` | `12` |
| `MappedDemo` | 2 GiB classic `map` limit, Arena mapping, resident memory | `13` |
| `Nio2BehaviourTest` | 12 assertions behind the claims above | `14` |
`01` records the environment (kernel, JDK, `ulimit -n`, inotify limit, free disk).
Timings move with whatever else the machine is doing; counts, exceptions and GC figures do not.
+18
View File
@@ -0,0 +1,18 @@
$ uname -sr
Linux 6.18.44-fc-v50
$ nproc
2
$ java -version
openjdk version "25.0.4.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
$ ulimit -n
20000
$ cat /proc/sys/fs/inotify/max_queued_events
16384
$ df -h (data directory, before)
Filesystem Size Used Avail Use% Mounted on
/dev/vda 252G 13G 29G 31% /
$ df -h (data directory, after deleting is done by the exit trap; shown here before)
Filesystem Size Used Avail Use% Mounted on
/dev/vda 252G 18G 25G 43% /
+8
View File
@@ -0,0 +1,8 @@
generated big.log: 5,000,000,069 bytes, 34,120,522 lines, 681,844 ERROR lines in 13.7 s
generated medium.log: 200,000,016 bytes, 1,378,768 lines, 27,530 ERROR lines in 1.4 s
$ ls -l (generated files)
5000000069 big.log
200000016 medium.log
$ df -h (data directory, with both files on disk)
Filesystem Size Used Avail Use% Mounted on
/dev/vda 252G 18G 25G 43% /
+17
View File
@@ -0,0 +1,17 @@
$ java -Xmx64m -Xlog:gc StreamLines lines big.log
Files.lines over big.log (5,000,000,069 bytes)
lines=34,120,522 errors=681,844 services=20
elapsed 9.4 s
heap: -Xmx=64 MiB, peak used (sampled every 20 ms)=38 MiB, GC collections=222, GC time=200 ms
GC log: 223 lines, 222 young pauses, 0 full GCs
first lines of the GC log:
[0.004s][info][gc] Using G1
[0.321s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 15M->1M(64M) 7.040ms
[0.440s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 28M->1M(64M) 1.218ms
[0.497s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 1.372ms
last lines of the GC log:
[9.358s][info][gc] GC(219) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.696ms
[9.396s][info][gc] GC(220) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.466ms
[9.430s][info][gc] GC(221) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.497ms
largest 'Pause Young' (ms): 7.040
highest heap-after-GC seen in the log: 1M of 64M
+17
View File
@@ -0,0 +1,17 @@
$ java -Xmx64m -Xlog:gc StreamLines reader big.log
Files.newBufferedReader over big.log (5,000,000,069 bytes)
lines=34,120,522 errors=681,844 services=20
elapsed 9.3 s
heap: -Xmx=64 MiB, peak used (sampled every 20 ms)=38 MiB, GC collections=222, GC time=212 ms
GC log: 223 lines, 222 young pauses, 0 full GCs
first lines of the GC log:
[0.005s][info][gc] Using G1
[0.273s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 15M->1M(64M) 2.473ms
[0.349s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 28M->1M(64M) 1.211ms
[0.401s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 1.173ms
last lines of the GC log:
[9.286s][info][gc] GC(219) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.515ms
[9.319s][info][gc] GC(220) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.427ms
[9.353s][info][gc] GC(221) Pause Young (Normal) (G1 Evacuation Pause) 38M->1M(64M) 0.469ms
largest 'Pause Young' (ms): 6.615
highest heap-after-GC seen in the log: 1M of 64M
+22
View File
@@ -0,0 +1,22 @@
$ java -Xmx64m ReadAllFail readAllLines medium.log
OutOfMemoryError: Java heap space
$ java -Xmx64m ReadAllFail readString medium.log
OutOfMemoryError: Java heap space
$ java -Xmx64m ReadAllFail readAllBytes medium.log
OutOfMemoryError: Java heap space
$ java -Xmx64m ReadAllFail readAllLines big.log
OutOfMemoryError: Java heap space
$ java -Xmx64m ReadAllFail readString big.log
OutOfMemoryError: Required array size too large
$ java -Xmx64m ReadAllFail readAllBytes big.log
OutOfMemoryError: Required array size too large
How much heap does the 200 MB file need? (readAllLines, then readString)
readAllLines -Xmx256m OutOfMemoryError: Java heap space
readString -Xmx256m survived: 200000016 chars
readAllLines -Xmx512m survived: 1378768 lines
readString -Xmx512m survived: 200000016 chars
readAllLines -Xmx1g survived: 1378768 lines
readString -Xmx1g survived: 200000016 chars
readAllLines -Xmx2g survived: 1378768 lines
readString -Xmx2g survived: 200000016 chars
+11
View File
@@ -0,0 +1,11 @@
open fds at start: 8
after 50 fully consumed, unclosed Files.walk: +0 fds
after 50 unclosed Files.lines + findFirst(): +50 fds
after closing them: +0 fds
after 50 try-with-resources Files.lines: +0 fds
after 50 unclosed Files.walk + findFirst(): +300 fds
after closing them: +0 fds
after 50 unreferenced, unclosed Files.lines: +50 fds
after System.gc(): +0 fds
after 50 unreferenced, unclosed Files.walk: +300 fds
after System.gc(): +300 fds
+3
View File
@@ -0,0 +1,3 @@
$ ulimit -n 64; java HandleExhaustion
failed on call 10 after 9 leaked walks
java.io.UncheckedIOException: java.nio.file.FileSystemException: <tmp>/tree/d2/sub: Too many open files
+9
View File
@@ -0,0 +1,9 @@
Files.list (one level): [.git, README.md, src]
Files.walk (everything): [, .git, .git/objects, .git/objects/blob.bin, README.md, src, src/main, src/main/App.java, src/test, src/test/AppTest.java]
Files.walk maxDepth=1: [, .git, README.md, src]
Files.find *.java: [src/main/App.java, src/test/AppTest.java]
Files.find size > 1000: [src/test/AppTest.java]
walkFileTree skipping .git: [README.md, src/main/App.java, src/test/AppTest.java]
DirectoryStream glob: [App.java]
walk with a symlink loop, default: 11 entries, no error
walk with FOLLOW_LINKS: FileSystemLoopException
+4
View File
@@ -0,0 +1,4 @@
lines processed before failure: 0
UncheckedIOException caused by java.nio.charset.MalformedInputException: Input length = 1
with ISO_8859_1: 3 lines, no error
readAllLines throws checked MalformedInputException
+19
View File
@@ -0,0 +1,19 @@
1. create a file, write it twice, delete it (events polled afterwards):
ENTRY_CREATE count=1 context=a.txt
ENTRY_MODIFY count=2 context=a.txt
ENTRY_DELETE count=1 context=a.txt
2. Files.writeString of 100 KiB to a new file (one call):
ENTRY_CREATE count=1 context=b.txt
ENTRY_MODIFY count=1 context=b.txt
3. 50 appends to one file with no polling in between, then poll:
{(batches)=1, ENTRY_MODIFY=9}
4. Files.move (atomic rename) of g.txt to h.txt:
ENTRY_DELETE count=1 context=g.txt
ENTRY_CREATE count=1 context=h.txt
5. a file created inside a NEW subdirectory (subdirectory not registered):
ENTRY_CREATE count=1 context=sub
6. burst: create N empty files in a fresh directory before polling once (jdk.nio.file.WatchService.maxEventsPerPoll=default):
N=100 delivered 100 events: ENTRY_CREATE=100 OVERFLOW(count)=0, files on disk=100
N=512 delivered 512 events: ENTRY_CREATE=512 OVERFLOW(count)=0, files on disk=512
N=513 delivered 1 events: ENTRY_CREATE=0 OVERFLOW(count)=1, files on disk=513
N=5000 delivered 1 events: ENTRY_CREATE=0 OVERFLOW(count)=4488, files on disk=5000
+13
View File
@@ -0,0 +1,13 @@
$ java -Djdk.nio.file.WatchService.maxEventsPerPoll=10000 WatchDemo (scenario 6 only)
6. burst: create N empty files in a fresh directory before polling once (jdk.nio.file.WatchService.maxEventsPerPoll=10000):
N=100 delivered 100 events: ENTRY_CREATE=100 OVERFLOW(count)=0, files on disk=100
N=512 delivered 512 events: ENTRY_CREATE=512 OVERFLOW(count)=0, files on disk=512
N=513 delivered 513 events: ENTRY_CREATE=513 OVERFLOW(count)=0, files on disk=513
N=5000 delivered 5000 events: ENTRY_CREATE=5000 OVERFLOW(count)=0, files on disk=5000
Scenario 3 (50 appends, no polling) over five more runs:
{(batches)=1, ENTRY_MODIFY=21}
{(batches)=1, ENTRY_MODIFY=20}
{(batches)=1, ENTRY_MODIFY=2}
{(batches)=1, ENTRY_MODIFY=1}
{(batches)=1, ENTRY_MODIFY=19}
@@ -0,0 +1,54 @@
final void signalEvent(WatchEvent.Kind<?> kind, Object context) {
boolean isModify = (kind == StandardWatchEventKinds.ENTRY_MODIFY);
synchronized (this) {
int size = events.size();
if (size > 0) {
// if the previous event is an OVERFLOW event or this is a
// repeated event then we simply increment the counter
WatchEvent<?> prev = events.get(size-1);
if ((prev.kind() == StandardWatchEventKinds.OVERFLOW) ||
((kind == prev.kind() &&
Objects.equals(context, prev.context()))))
{
((Event<?>)prev).increment();
return;
}
// if this is a modify event and the last entry for the context
// is a modify event then we simply increment the count
if (!lastModifyEvents.isEmpty()) {
if (isModify) {
WatchEvent<?> ev = lastModifyEvents.get(context);
if (ev != null) {
assert ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY;
((Event<?>)ev).increment();
return;
}
} else {
// not a modify event so remove from the map as the
// last event will no longer be a modify event.
lastModifyEvents.remove(context);
}
}
// if the list has reached the limit then drop pending events
// and queue an OVERFLOW event
if (size >= MAX_EVENT_LIST_SIZE) {
kind = StandardWatchEventKinds.OVERFLOW;
isModify = false;
context = null;
}
}
// non-repeated event
Event<Object> ev =
new Event<>((WatchEvent.Kind<Object>)kind, context);
if (isModify) {
lastModifyEvents.put(context, ev);
} else if (kind == StandardWatchEventKinds.OVERFLOW) {
// drop all pending events
events.clear();
lastModifyEvents.clear();
}
events.add(ev);
signal();
+7
View File
@@ -0,0 +1,7 @@
$ java -Xmx64m MappedDemo big.log
file big.log: 5,000,000,069 bytes, -Xmx=64 MiB, RSS at start=40 MiB
classic map of the whole file: IllegalArgumentException: Size exceeds Integer.MAX_VALUE
Arena + MemorySegment: 34,120,522 newlines in 5.1 s; RSS after the Arena was closed=51 MiB
classic 1 GiB windows: 34,120,522 newlines in 7.8 s; RSS right after=4821 MiB
heap: -Xmx=64 MiB, peak used (sampled every 20 ms)=4 MiB, GC collections=0, GC time=0 ms
RSS one second after System.gc()=52 MiB
+4
View File
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.ankurm.nio2.Nio2BehaviourTest
-------------------------------------------------------------------------------
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.755 s -- in com.ankurm.nio2.Nio2BehaviourTest
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm</groupId>
<artifactId>java-core-examples</artifactId>
<version>1.0</version>
</parent>
<artifactId>nio2</artifactId>
<name>nio2</name>
<description>Java NIO.2 file API: streaming a multi-gigabyte file at constant heap, readAllLines/readString failure modes, unclosed Files.lines/Files.walk handles, WatchService behaviour on Linux, and memory-mapped files.</description>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Regenerates every file in ../output/. Requires JDK25_HOME. Needs ~5.3 GB of free disk in $DATA (default /tmp/nio2-data);
# the generated files are deleted on exit. Set BIG_BYTES to use a smaller "big" file (default 5 GB).
set -euo pipefail
[[ -z "${JDK25_HOME:-}" ]] && { echo "JDK25_HOME must be set" >&2; exit 1; }
cd "$(dirname "$0")/.."
OUT=output; mkdir -p "$OUT"
DATA="${DATA:-/tmp/nio2-data}"; mkdir -p "$DATA"
BIG_BYTES="${BIG_BYTES:-5000000000}"
MED_BYTES=200000000
trap 'rm -rf "$DATA"' EXIT
export JAVA_HOME="$JDK25_HOME"
mvn -q -f ../pom.xml -pl nio2 -am package
J="$JDK25_HOME/bin/java"; CP=target/classes
strip() { grep -v "Picked up"; }
{ echo '$ uname -sr'; uname -sr; echo '$ nproc'; nproc; echo '$ java -version'; "$J" -version 2>&1 | strip
echo '$ ulimit -n'; ulimit -n
echo '$ cat /proc/sys/fs/inotify/max_queued_events'; cat /proc/sys/fs/inotify/max_queued_events
echo '$ df -h (data directory, before)'; df -h "$DATA" | sed 's/ */ /g'; } > "$OUT/01-environment.txt"
echo "==> generate files"
{ "$J" -cp $CP com.ankurm.nio2.LogFile "$DATA/big.log" "$BIG_BYTES" 2>&1 | strip
"$J" -cp $CP com.ankurm.nio2.LogFile "$DATA/medium.log" "$MED_BYTES" 2>&1 | strip
echo '$ ls -l (generated files)'; ls -l "$DATA"/*.log | awk '{print $5, $9}' | sed "s#$DATA/##"
echo '$ df -h (data directory, with both files on disk)'; df -h "$DATA" | sed 's/ */ /g'; } > "$OUT/02-generate.txt"
echo "==> stream the big file at -Xmx64m"
for m in lines reader; do
n=$([[ $m == lines ]] && echo 03 || echo 04)
{ echo "\$ java -Xmx64m -Xlog:gc StreamLines $m big.log"
"$J" -Xmx64m -Xlog:gc:file="$DATA/gc-$m.log" -cp $CP com.ankurm.nio2.StreamLines $m "$DATA/big.log" 2>&1 | strip
echo "GC log: $(wc -l < "$DATA/gc-$m.log") lines, $(grep -c 'Pause Young' "$DATA/gc-$m.log") young pauses, $(grep -c 'Pause Full' "$DATA/gc-$m.log") full GCs"
echo "first lines of the GC log:"; head -4 "$DATA/gc-$m.log" | cut -c1-150
echo "last lines of the GC log:"; tail -3 "$DATA/gc-$m.log" | cut -c1-150
echo "largest 'Pause Young' (ms): $(grep 'Pause Young' "$DATA/gc-$m.log" | sed 's/.* \([0-9.]*\)ms$/\1/' | sort -n | tail -1)"
echo "highest heap-after-GC seen in the log: $(grep 'Pause Young' "$DATA/gc-$m.log" | sed 's/.*->\([0-9]*\)M(.*/\1/' | sort -n | tail -1)M of $(grep -m1 'Pause Young' "$DATA/gc-$m.log" | sed 's/.*M(\([0-9]*\)M).*/\1/')M"; } > "$OUT/$n-stream-$m.txt"
done
echo "==> readAllLines / readString / readAllBytes"
{ for f in medium big; do
for m in readAllLines readString readAllBytes; do
echo "\$ java -Xmx64m ReadAllFail $m $f.log"; "$J" -Xmx64m -cp $CP com.ankurm.nio2.ReadAllFail $m "$DATA/$f.log" 2>&1 | strip | tail -n +2; done; done
echo; echo "How much heap does the 200 MB file need? (readAllLines, then readString)"
for x in 256m 512m 1g 2g; do
for m in readAllLines readString; do
printf '%-13s -Xmx%-5s ' "$m" "$x"; "$J" -Xmx$x -cp $CP com.ankurm.nio2.ReadAllFail $m "$DATA/medium.log" 2>&1 | strip | tail -n 1; done; done; } > "$OUT/05-read-all-fail.txt"
echo "==> handles"
"$J" -cp $CP com.ankurm.nio2.HandleLeakDemo 2>&1 | strip > "$OUT/06-handle-leak.txt"
{ echo '$ ulimit -n 64; java HandleExhaustion'; (ulimit -n 64; "$J" -cp $CP com.ankurm.nio2.HandleExhaustion 2>&1 | strip); } > "$OUT/07-handle-exhaustion.txt"
echo "==> walk / find"
"$J" -cp $CP com.ankurm.nio2.WalkFindDemo 2>&1 | strip > "$OUT/08-walk-find.txt"
"$J" -cp $CP com.ankurm.nio2.LinesCharsetDemo 2>&1 | strip > "$OUT/09-lines-charset.txt"
echo "==> WatchService"
"$J" -cp $CP com.ankurm.nio2.WatchDemo 2>&1 | strip > "$OUT/10-watch.txt"
{ echo '$ java -Djdk.nio.file.WatchService.maxEventsPerPoll=10000 WatchDemo (scenario 6 only)'
"$J" -Djdk.nio.file.WatchService.maxEventsPerPoll=10000 -cp $CP com.ankurm.nio2.WatchDemo 2>&1 | strip | sed -n '/^6\./,$p'
echo; echo "Scenario 3 (50 appends, no polling) over five more runs:"
for i in 1 2 3 4 5; do "$J" -cp $CP com.ankurm.nio2.WatchDemo 2>&1 | strip | sed -n '/^3\./,/^4\./p' | sed '2!d'; done; } > "$OUT/11-watch-tuned-and-repeat.txt"
unzip -p "$JDK25_HOME/lib/src.zip" java.base/sun/nio/fs/AbstractWatchKey.java | sed -n '/final void signalEvent/,/signal();/p' > "$OUT/12-watchkey-source-excerpt.txt"
echo "==> memory-mapped"
{ echo '$ java -Xmx64m MappedDemo big.log'; "$J" -Xmx64m -cp $CP com.ankurm.nio2.MappedDemo "$DATA/big.log" 2>&1 | strip; } > "$OUT/13-mapped.txt"
cp target/surefire-reports/com.ankurm.nio2.Nio2BehaviourTest.txt "$OUT/14-tests.txt"
{ echo '$ df -h (data directory, after deleting is done by the exit trap; shown here before)'; df -h "$DATA" | sed 's/ */ /g'; } >> "$OUT/01-environment.txt"
echo Done
@@ -0,0 +1,27 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/** Run under a low `ulimit -n` to see what a leak turns into. */
public final class HandleExhaustion {
private HandleExhaustion() {}
public static void main(String[] a) throws IOException {
Path tmp = Files.createTempDirectory("nio2-exhaust");
Path tree = HandleLeakDemo.makeTree(tmp.resolve("tree"));
List<Stream<Path>> leaked = new ArrayList<>();
int i = 0;
try {
for (; ; i++) { Stream<Path> s = Files.walk(tree); s.skip(3).findFirst(); leaked.add(s); }
} catch (Exception e) {
int n = leaked.size();
System.out.printf("failed on call %d after %d leaked walks%n%s: %s%n", i + 1, n,
e.getClass().getName(), e.getMessage() == null ? "" : e.getMessage().replace(tmp.toString(), "<tmp>"));
}
}
}
@@ -0,0 +1,68 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/** Counts this process's open file descriptors (Linux /proc/self/fd) around unclosed Files.lines / Files.walk. */
public final class HandleLeakDemo {
private HandleLeakDemo() {}
public static int openFds() throws IOException {
try (Stream<Path> s = Files.list(Path.of("/proc/self/fd"))) { return (int) s.count(); }
}
static Path makeTree(Path root) throws IOException {
for (int i = 0; i < 5; i++) {
Path d = Files.createDirectories(root.resolve("d" + i).resolve("sub"));
Files.writeString(d.resolve("f.txt"), "x\n");
Files.writeString(root.resolve("d" + i).resolve("g.txt"), "y\n");
}
return root;
}
public static void main(String[] a) throws Exception {
Path tmp = Files.createTempDirectory("nio2-leak");
Path file = Files.writeString(tmp.resolve("one.txt"), "a\nb\nc\n");
Path tree = makeTree(tmp.resolve("tree"));
final int N = 50;
int base = openFds();
System.out.println("open fds at start: " + base);
// 0. Files.walk fully consumed but not closed, run first so later leaks do not hide it.
for (int i = 0; i < N; i++) { Files.walk(tree).count(); }
System.out.printf("after %d fully consumed, unclosed Files.walk: +%d fds%n", N, openFds() - base);
// 1. Files.lines, opened and read partially, never closed. References are kept (as a field or cache would).
List<Stream<String>> keep = new ArrayList<>();
for (int i = 0; i < N; i++) { Stream<String> s = Files.lines(file); s.findFirst(); keep.add(s); }
System.out.printf("after %d unclosed Files.lines + findFirst(): +%d fds%n", N, openFds() - base);
keep.forEach(Stream::close);
System.out.printf("after closing them: +%d fds%n", openFds() - base);
// 2. Files.lines in try-with-resources, same work.
for (int i = 0; i < N; i++) { try (Stream<String> s = Files.lines(file)) { s.findFirst(); } }
System.out.printf("after %d try-with-resources Files.lines: +%d fds%n", N, openFds() - base);
// 3. Files.walk, findFirst(), never closed.
List<Stream<Path>> walks = new ArrayList<>();
for (int i = 0; i < N; i++) { Stream<Path> s = Files.walk(tree); s.skip(3).findFirst(); walks.add(s); }
System.out.printf("after %d unclosed Files.walk + findFirst(): +%d fds%n", N, openFds() - base);
walks.forEach(Stream::close);
System.out.printf("after closing them: +%d fds%n", openFds() - base);
// 3b. The same unclosed Files.lines, but nobody keeps a reference: does the garbage collector clean up?
for (int i = 0; i < N; i++) { Files.lines(file).findFirst(); }
System.out.printf("after %d unreferenced, unclosed Files.lines: +%d fds%n", N, openFds() - base);
System.gc(); Thread.sleep(500);
System.out.printf("after System.gc(): +%d fds%n", openFds() - base);
for (int i = 0; i < N; i++) { Files.walk(tree).skip(3).findFirst(); }
System.out.printf("after %d unreferenced, unclosed Files.walk: +%d fds%n", N, openFds() - base);
System.gc(); Thread.sleep(500);
System.out.printf("after System.gc(): +%d fds%n", openFds() - base);
}
}
@@ -0,0 +1,48 @@
package com.ankurm.nio2;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
/** Samples used heap on a daemon thread and reports peak-used, max heap and GC counts. */
public final class HeapProbe implements AutoCloseable {
private volatile boolean run = true;
private volatile long peak;
private final Thread t;
public HeapProbe() {
t = new Thread(() -> {
Runtime rt = Runtime.getRuntime();
while (run) {
long used = rt.totalMemory() - rt.freeMemory();
if (used > peak) peak = used;
try { Thread.sleep(20); } catch (InterruptedException e) { return; }
}
}, "heap-probe");
t.setDaemon(true);
t.start();
}
public long peakUsed() { return peak; }
public String report() {
long gcs = 0, ms = 0;
for (GarbageCollectorMXBean b : ManagementFactory.getGarbageCollectorMXBeans()) {
gcs += b.getCollectionCount(); ms += b.getCollectionTime();
}
return String.format("heap: -Xmx=%d MiB, peak used (sampled every 20 ms)=%d MiB, GC collections=%d, GC time=%d ms",
Runtime.getRuntime().maxMemory() >> 20, peak >> 20, gcs, ms);
}
@Override public void close() { run = false; }
/** Resident set size of this process in MiB (Linux only), or -1. */
public static long rssMiB() {
try {
for (String l : Files.readAllLines(Path.of("/proc/self/status")))
if (l.startsWith("VmRSS:")) return Long.parseLong(l.replaceAll("\\D+", "")) >> 10;
} catch (Exception ignored) { }
return -1;
}
}
@@ -0,0 +1,36 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
/** Files.lines is lazy, so a bad byte fails in the middle of the pipeline, as an unchecked exception. */
public final class LinesCharsetDemo {
private LinesCharsetDemo() {}
public static void main(String[] a) throws IOException {
Path p = Files.createTempFile("nio2-charset", ".txt");
byte[] ok = "good line 1\ngood line 2\n".getBytes(StandardCharsets.UTF_8);
byte[] bad = {(byte) 0xE9, '\n'}; // 0xE9 is 'e-acute' in ISO-8859-1 but not valid UTF-8 here
Files.write(p, ok);
Files.write(p, bad, java.nio.file.StandardOpenOption.APPEND);
int[] seen = {0};
try (Stream<String> s = Files.lines(p)) {
s.forEach(l -> seen[0]++);
} catch (UncheckedIOException e) {
System.out.println("lines processed before failure: " + seen[0]);
System.out.println(e.getClass().getSimpleName() + " caused by " + e.getCause());
}
try (Stream<String> s = Files.lines(p, StandardCharsets.ISO_8859_1)) {
System.out.println("with ISO_8859_1: " + s.count() + " lines, no error");
}
try {
Files.readAllLines(p);
} catch (java.nio.charset.MalformedInputException e) {
System.out.println("readAllLines throws checked " + e.getClass().getSimpleName());
}
}
}
@@ -0,0 +1,46 @@
package com.ankurm.nio2;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/** Generates a deterministic synthetic log file of (at least) a requested size. One line = one record. */
public final class LogFile {
private LogFile() {}
public record Stats(long bytes, long lines, long errors) {}
/** Writes whole lines until at least {@code targetBytes} have been written. */
public static Stats generate(Path target, long targetBytes) throws IOException {
long seed = 42, bytes = 0, lines = 0, errors = 0;
try (BufferedWriter w = Files.newBufferedWriter(target)) {
StringBuilder sb = new StringBuilder(200);
while (bytes < targetBytes) {
seed = seed * 6364136223846793005L + 1442695040888963407L;
int r = (int) (seed >>> 33);
boolean error = r % 50 == 0;
if (error) errors++;
sb.setLength(0);
sb.append("2026-01-01T00:00:").append(String.format("%02d", (lines / 1000) % 60))
.append(' ').append(error ? "ERROR" : "INFO ")
.append(" service-").append(r % 20)
.append(" request handled id=").append(lines)
.append(" user=").append(Integer.toHexString(r))
.append(" payload=").append("x".repeat(40 + r % 40)).append('\n');
w.write(sb.toString());
bytes += sb.length(); // ASCII only, so chars == bytes
lines++;
}
}
return new Stats(bytes, lines, errors);
}
public static void main(String[] a) throws IOException {
Path p = Path.of(a[0]);
long t0 = System.nanoTime();
Stats s = generate(p, Long.parseLong(a[1]));
System.out.printf("generated %s: %,d bytes, %,d lines, %,d ERROR lines in %.1f s%n",
p.getFileName(), s.bytes(), s.lines(), s.errors(), (System.nanoTime() - t0) / 1e9);
}
}
@@ -0,0 +1,67 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/** Memory-mapped reading: classic MappedByteBuffer (2 GiB cap per mapping) vs FileChannel.map with an Arena. */
public final class MappedDemo {
private MappedDemo() {}
/** Counts '\n' with an Arena-backed mapping of the whole file: no 2 GiB limit, no heap copy. */
public static long countNewlinesArena(Path p) throws IOException {
try (Arena arena = Arena.ofConfined();
FileChannel ch = FileChannel.open(p, StandardOpenOption.READ)) {
MemorySegment seg = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size(), arena);
long n = 0;
for (long i = 0, len = seg.byteSize(); i < len; i++) if (seg.get(ValueLayout.JAVA_BYTE, i) == '\n') n++;
return n;
}
}
/** Same with the classic API, in windows of at most 1 GiB. */
public static long countNewlinesWindows(Path p) throws IOException {
long n = 0;
try (FileChannel ch = FileChannel.open(p, StandardOpenOption.READ)) {
long size = ch.size(), win = 1L << 30;
for (long off = 0; off < size; off += win) {
MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, off, Math.min(win, size - off));
for (int i = 0, lim = mb.limit(); i < lim; i++) if (mb.get(i) == '\n') n++;
}
}
return n;
}
public static void main(String[] a) throws IOException {
Path p = Path.of(a[0]);
long size = Files.size(p);
System.out.printf("file %s: %,d bytes, -Xmx=%d MiB, RSS at start=%d MiB%n", p.getFileName(), size,
Runtime.getRuntime().maxMemory() >> 20, HeapProbe.rssMiB());
try (FileChannel ch = FileChannel.open(p, StandardOpenOption.READ)) {
ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size());
System.out.println("classic map of the whole file: succeeded");
} catch (IllegalArgumentException e) {
System.out.println("classic map of the whole file: IllegalArgumentException: " + e.getMessage());
}
try (HeapProbe probe = new HeapProbe()) {
long t0 = System.nanoTime();
long n2 = countNewlinesArena(p);
System.out.printf("Arena + MemorySegment: %,d newlines in %.1f s; RSS after the Arena was closed=%d MiB%n",
n2, (System.nanoTime() - t0) / 1e9, HeapProbe.rssMiB());
t0 = System.nanoTime();
long n1 = countNewlinesWindows(p);
System.out.printf("classic 1 GiB windows: %,d newlines in %.1f s; RSS right after=%d MiB%n",
n1, (System.nanoTime() - t0) / 1e9, HeapProbe.rssMiB());
System.out.println(probe.report());
System.gc();
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("RSS one second after System.gc()=" + HeapProbe.rssMiB() + " MiB");
}
}
}
@@ -0,0 +1,28 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/** The convenience methods that load the whole file. Run with a small -Xmx to see the failure. */
public final class ReadAllFail {
private ReadAllFail() {}
public static void main(String[] a) throws IOException {
Path p = Path.of(a[1]);
System.out.printf("%s on %s (%,d bytes), -Xmx=%d MiB%n", a[0], p.getFileName(), Files.size(p),
Runtime.getRuntime().maxMemory() >> 20);
try {
Object r = switch (a[0]) {
case "readAllLines" -> Files.readAllLines(p);
case "readString" -> Files.readString(p);
case "readAllBytes" -> Files.readAllBytes(p);
default -> throw new IllegalArgumentException(a[0]);
};
System.out.println("survived: " + (r instanceof java.util.List<?> l ? l.size() + " lines"
: r instanceof String s ? s.length() + " chars" : ((byte[]) r).length + " bytes"));
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError: " + e.getMessage());
}
}
}
@@ -0,0 +1,58 @@
package com.ankurm.nio2;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
/** Processes a file of any size in constant heap: count lines, ERROR lines, and lines per service. */
public final class StreamLines {
private StreamLines() {}
public record Result(long lines, long errors, Map<String, Long> perService) {}
/** Files.lines: lazy Stream<String>; MUST be closed (it owns a file handle). */
public static Result withFilesLines(Path p) throws IOException {
long[] c = new long[2];
Map<String, Long> per = new TreeMap<>();
try (Stream<String> s = Files.lines(p)) {
s.forEach(l -> tally(l, c, per));
}
return new Result(c[0], c[1], per);
}
/** Files.newBufferedReader: the same thing with a plain loop. */
public static Result withBufferedReader(Path p) throws IOException {
long[] c = new long[2];
Map<String, Long> per = new TreeMap<>();
try (BufferedReader r = Files.newBufferedReader(p)) {
for (String l = r.readLine(); l != null; l = r.readLine()) tally(l, c, per);
}
return new Result(c[0], c[1], per);
}
private static void tally(String line, long[] c, Map<String, Long> per) {
c[0]++;
if (line.startsWith("ERROR", 20)) c[1]++;
int i = line.indexOf("service-");
per.merge(line.substring(i, line.indexOf(' ', i)), 1L, Long::sum);
}
public static void main(String[] a) throws IOException {
Path p = Path.of(a[1]);
long t0 = System.nanoTime();
Result r;
try (HeapProbe probe = new HeapProbe()) {
r = a[0].equals("lines") ? withFilesLines(p) : withBufferedReader(p);
double s = (System.nanoTime() - t0) / 1e9;
System.out.printf("%s over %s (%,d bytes)%n", a[0].equals("lines") ? "Files.lines" : "Files.newBufferedReader",
p.getFileName(), Files.size(p));
System.out.printf("lines=%,d errors=%,d services=%d%n", r.lines(), r.errors(), r.perService().size());
System.out.printf("elapsed %.1f s%n", s);
System.out.println(probe.report());
}
}
}
@@ -0,0 +1,64 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** Files.list / walk / find / walkFileTree / DirectoryStream on a small tree. */
public final class WalkFindDemo {
private WalkFindDemo() {}
static List<String> rel(Path root, Stream<Path> s) {
try (s) { return s.map(p -> root.relativize(p).toString()).sorted().collect(Collectors.toList()); }
}
public static void main(String[] a) throws IOException {
Path root = Files.createTempDirectory("nio2-walk");
Files.createDirectories(root.resolve("src/main"));
Files.createDirectories(root.resolve("src/test"));
Files.createDirectories(root.resolve(".git/objects"));
Files.writeString(root.resolve("README.md"), "# demo\n");
Files.writeString(root.resolve("src/main/App.java"), "class App {}\n");
Files.writeString(root.resolve("src/test/AppTest.java"), "class AppTest {}\n" + "x".repeat(2000));
Files.writeString(root.resolve(".git/objects/blob.bin"), "ignored");
System.out.println("Files.list (one level): " + rel(root, Files.list(root)));
System.out.println("Files.walk (everything): " + rel(root, Files.walk(root)));
System.out.println("Files.walk maxDepth=1: " + rel(root, Files.walk(root, 1)));
System.out.println("Files.find *.java: " + rel(root,
Files.find(root, 10, (p, at) -> at.isRegularFile() && p.toString().endsWith(".java"))));
System.out.println("Files.find size > 1000: " + rel(root,
Files.find(root, 10, (p, at) -> at.isRegularFile() && at.size() > 1000)));
List<String> seen = new java.util.ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override public FileVisitResult preVisitDirectory(Path d, BasicFileAttributes at) {
return d.getFileName().toString().equals(".git") ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
}
@Override public FileVisitResult visitFile(Path f, BasicFileAttributes at) {
seen.add(root.relativize(f).toString()); return FileVisitResult.CONTINUE;
}
});
java.util.Collections.sort(seen);
System.out.println("walkFileTree skipping .git: " + seen);
List<String> glob = new java.util.ArrayList<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(root.resolve("src/main"), "*.{java,kt}")) {
ds.forEach(p -> glob.add(p.getFileName().toString()));
}
System.out.println("DirectoryStream glob: " + glob);
// A symlink loop: walk() does not follow links by default; with FOLLOW_LINKS it detects the cycle.
Files.createSymbolicLink(root.resolve("src/loop"), root);
System.out.println("walk with a symlink loop, default: " + rel(root, Files.walk(root)).size() + " entries, no error");
try (Stream<Path> s = Files.walk(root, FileVisitOption.FOLLOW_LINKS)) {
s.forEach(p -> { });
} catch (UncheckedIOException e) {
System.out.println("walk with FOLLOW_LINKS: " + e.getCause().getClass().getSimpleName());
}
}
}
@@ -0,0 +1,82 @@
package com.ankurm.nio2;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static java.nio.file.StandardWatchEventKinds.*;
/** What WatchService actually delivers on this platform. Every scenario prints what it saw. */
public final class WatchDemo {
private WatchDemo() {}
/** Drains the key until no new event arrives for quietMs; returns kind -> total repeat-count. */
static Map<String, Integer> drain(WatchService ws, long quietMs, boolean verbose) throws Exception {
Map<String, Integer> tally = new TreeMap<>();
int batches = 0;
WatchKey k;
while ((k = ws.poll(quietMs, TimeUnit.MILLISECONDS)) != null) {
batches++;
List<WatchEvent<?>> evs = k.pollEvents();
for (WatchEvent<?> e : evs) {
tally.merge(e.kind().name(), e.count(), Integer::sum);
if (verbose) System.out.println(" " + e.kind().name() + " count=" + e.count() + " context=" + e.context());
}
k.reset();
}
tally.put("(batches)", batches);
return tally;
}
public static void main(String[] a) throws Exception {
Path dir = Files.createTempDirectory("nio2-watch");
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
dir.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
System.out.println("1. create a file, write it twice, delete it (events polled afterwards):");
Path f = dir.resolve("a.txt");
Files.writeString(f, "one");
Files.writeString(f, "two", StandardOpenOption.APPEND);
Files.delete(f);
drain(ws, 300, true);
System.out.println("2. Files.writeString of 100 KiB to a new file (one call):");
Files.writeString(dir.resolve("b.txt"), "y".repeat(100 * 1024));
drain(ws, 300, true);
System.out.println("3. 50 appends to one file with no polling in between, then poll:");
Path g = dir.resolve("g.txt");
Files.writeString(g, "");
drain(ws, 300, false);
for (int i = 0; i < 50; i++) Files.writeString(g, "line\n", StandardOpenOption.APPEND);
System.out.println(" " + drain(ws, 300, false));
System.out.println("4. Files.move (atomic rename) of g.txt to h.txt:");
Files.move(g, dir.resolve("h.txt"), StandardCopyOption.ATOMIC_MOVE);
drain(ws, 300, true);
System.out.println("5. a file created inside a NEW subdirectory (subdirectory not registered):");
Path sub = Files.createDirectory(dir.resolve("sub"));
Files.writeString(sub.resolve("deep.txt"), "hello");
drain(ws, 300, true);
System.out.println("6. burst: create N empty files in a fresh directory before polling once"
+ " (jdk.nio.file.WatchService.maxEventsPerPoll=" + System.getProperty("jdk.nio.file.WatchService.maxEventsPerPoll", "default") + "):");
for (int n : new int[] {100, 512, 513, 5000}) {
Path burst = Files.createDirectory(dir.resolve("burst" + n));
try (WatchService ws2 = FileSystems.getDefault().newWatchService()) {
burst.register(ws2, ENTRY_CREATE);
for (int i = 0; i < n; i++) Files.createFile(burst.resolve("f" + i));
Thread.sleep(500);
WatchKey k = ws2.poll(1, TimeUnit.SECONDS);
List<WatchEvent<?>> evs = k.pollEvents();
long creates = evs.stream().filter(e -> e.kind() == ENTRY_CREATE).count();
long overflow = evs.stream().filter(e -> e.kind() == OVERFLOW).mapToInt(WatchEvent::count).sum();
System.out.printf(" N=%-5d delivered %-4d events: ENTRY_CREATE=%-4d OVERFLOW(count)=%d, files on disk=%d%n",
n, evs.size(), creates, overflow, Files.list(burst).count());
}
}
}
}
}
@@ -0,0 +1,183 @@
package com.ankurm.nio2;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UncheckedIOException;
import java.nio.channels.FileChannel;
import java.nio.charset.MalformedInputException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static java.nio.file.StandardWatchEventKinds.*;
import static org.junit.jupiter.api.Assertions.*;
class Nio2BehaviourTest {
@TempDir Path tmp;
private Path sample() throws IOException {
Path p = tmp.resolve("sample.log");
LogFile.generate(p, 2_000_000);
return p;
}
@Test
void streamingMatchesReadAllLines() throws IOException {
Path p = sample();
List<String> all = Files.readAllLines(p);
assertEquals(all.size(), StreamLines.withFilesLines(p).lines());
assertEquals(all.size(), StreamLines.withBufferedReader(p).lines());
assertEquals(all.stream().filter(l -> l.startsWith("ERROR", 20)).count(), StreamLines.withFilesLines(p).errors());
}
@Test
void generatorStatsMatchTheFile() throws IOException {
Path p = tmp.resolve("g.log");
LogFile.Stats s = LogFile.generate(p, 500_000);
assertEquals(Files.size(p), s.bytes());
try (Stream<String> l = Files.lines(p)) { assertEquals(s.lines(), l.count()); }
}
@Test
void mappedCountersAgreeWithLineCount() throws IOException {
Path p = sample();
long lines = StreamLines.withFilesLines(p).lines();
assertEquals(lines, MappedDemo.countNewlinesArena(p));
assertEquals(lines, MappedDemo.countNewlinesWindows(p));
}
@Test
void classicMapRejectsMoreThan2GiBButArenaMapsIt() throws IOException {
Path sparse = tmp.resolve("sparse.bin");
try (RandomAccessFile f = new RandomAccessFile(sparse.toFile(), "rw")) { f.setLength(3L << 30); } // sparse: no real disk used
try (FileChannel ch = FileChannel.open(sparse, StandardOpenOption.READ)) {
assertThrows(IllegalArgumentException.class, () -> ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size()));
}
try (var arena = java.lang.foreign.Arena.ofConfined(); FileChannel ch = FileChannel.open(sparse, StandardOpenOption.READ)) {
var seg = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size(), arena);
assertEquals(3L << 30, seg.byteSize());
}
}
private static boolean linux() { return Files.isDirectory(Path.of("/proc/self/fd")); }
@Test
void unclosedFilesLinesHoldsAFileDescriptorUntilClosed() throws IOException {
Assumptions.assumeTrue(linux());
Path p = sample();
int base = HandleLeakDemo.openFds();
List<Stream<String>> keep = new ArrayList<>();
for (int i = 0; i < 10; i++) { Stream<String> s = Files.lines(p); s.findFirst(); keep.add(s); }
assertEquals(base + 10, HandleLeakDemo.openFds());
keep.forEach(Stream::close);
assertEquals(base, HandleLeakDemo.openFds());
for (int i = 0; i < 10; i++) { try (Stream<String> s = Files.lines(p)) { s.findFirst(); } }
assertEquals(base, HandleLeakDemo.openFds());
}
@Test
void walkStoppedEarlyHoldsDirectoriesOpenButFullyConsumedWalkDoesNot() throws IOException {
Assumptions.assumeTrue(linux());
Path tree = HandleLeakDemo.makeTree(tmp.resolve("tree"));
int base = HandleLeakDemo.openFds();
Stream<Path> early = Files.walk(tree);
early.skip(3).findFirst();
assertTrue(HandleLeakDemo.openFds() > base, "an unfinished walk should hold directory handles");
early.close();
assertEquals(base, HandleLeakDemo.openFds());
Files.walk(tree).count();
assertEquals(base, HandleLeakDemo.openFds());
}
@Test
void badByteSurfacesLazilyAsUncheckedIOException() throws IOException {
Path p = tmp.resolve("bad.txt");
Files.write(p, new byte[] {'o', 'k', '\n', (byte) 0xE9, '\n'});
try (Stream<String> s = Files.lines(p)) {
UncheckedIOException e = assertThrows(UncheckedIOException.class, () -> s.forEach(l -> { }));
assertInstanceOf(MalformedInputException.class, e.getCause());
}
assertThrows(MalformedInputException.class, () -> Files.readAllLines(p));
}
@Test
void walkFindAndSkipSubtree() throws IOException {
Files.createDirectories(tmp.resolve("a/b"));
Files.writeString(tmp.resolve("a/b/x.java"), "x");
Files.writeString(tmp.resolve("a/y.txt"), "y");
try (Stream<Path> s = Files.find(tmp, 5, (p, at) -> at.isRegularFile() && p.toString().endsWith(".java"))) {
assertEquals(1, s.count());
}
try (Stream<Path> s = Files.walk(tmp, 1)) { assertEquals(2, s.count()); } // tmp itself + "a"
}
@Test
void watchServiceDeliversCreateAndCoalescesRepeatedModify() throws Exception {
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
tmp.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
Path f = tmp.resolve("w.txt");
Files.writeString(f, "x".repeat(100 * 1024));
Thread.sleep(300);
WatchKey k = ws.poll(2, TimeUnit.SECONDS);
assertNotNull(k);
List<WatchEvent<?>> evs = k.pollEvents();
assertEquals(ENTRY_CREATE, evs.get(0).kind());
assertEquals(f.getFileName(), evs.get(0).context());
assertTrue(evs.size() <= 2, "MODIFY events for one file collapse into one event with a count: " + evs);
}
}
@Test
void watchKeyOverflowsPast512PendingEventsAndDropsThem() throws Exception {
Assumptions.assumeTrue(System.getProperty("jdk.nio.file.WatchService.maxEventsPerPoll") == null);
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
tmp.register(ws, ENTRY_CREATE);
for (int i = 0; i < 513; i++) Files.createFile(tmp.resolve("f" + i));
Thread.sleep(500);
WatchKey k = ws.poll(2, TimeUnit.SECONDS);
List<WatchEvent<?>> evs = k.pollEvents();
assertEquals(1, evs.size());
assertEquals(OVERFLOW, evs.get(0).kind());
}
}
@Test
void watchServiceDoesNotWatchSubdirectoriesRecursively() throws Exception {
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
tmp.register(ws, ENTRY_CREATE);
Path sub = Files.createDirectory(tmp.resolve("sub"));
Thread.sleep(200);
WatchKey k = ws.poll(2, TimeUnit.SECONDS);
k.pollEvents(); k.reset();
Files.writeString(sub.resolve("deep.txt"), "x");
assertNull(ws.poll(500, TimeUnit.MILLISECONDS), "no event for a file in an unregistered subdirectory");
}
}
@Test
void readAllLinesFailsWhereStreamingSurvivesUnderSmallHeap() throws Exception {
// Runs the real programs in child JVMs so -Xmx is honoured.
Path p = tmp.resolve("m.log");
LogFile.generate(p, 100_000_000);
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
String cp = System.getProperty("java.class.path");
String streamOut = run(java, "-Xmx32m", "-cp", cp, "com.ankurm.nio2.StreamLines", "lines", p.toString());
assertTrue(streamOut.contains("lines="), streamOut);
String failOut = run(java, "-Xmx32m", "-cp", cp, "com.ankurm.nio2.ReadAllFail", "readAllLines", p.toString());
assertTrue(failOut.contains("OutOfMemoryError"), failOut);
}
private static String run(String... cmd) throws Exception {
Process pr = new ProcessBuilder(cmd).redirectErrorStream(true).start();
String s = new String(pr.getInputStream().readAllBytes());
pr.waitFor();
return s;
}
}
+1
View File
@@ -27,6 +27,7 @@
<module>serialization</module>
<module>strings</module>
<module>collections</module>
<module>nio2</module>
</modules>
<properties>