33import com .maxmind .db .Reader .FileMode ;
44import java .io .ByteArrayOutputStream ;
55import java .io .File ;
6+ import java .io .FileInputStream ;
67import java .io .IOException ;
78import java .io .InputStream ;
89import java .io .RandomAccessFile ;
@@ -23,53 +24,36 @@ final class BufferHolder {
2324 }
2425
2526 BufferHolder (File database , FileMode mode , int chunkSize ) throws IOException {
26- try (RandomAccessFile file = new RandomAccessFile (database , "r" );
27- FileChannel channel = file .getChannel ()) {
28- long size = channel .size ();
29- if (mode == FileMode .MEMORY ) {
27+ if (mode == FileMode .MEMORY ) {
28+ // FileInputStream avoids the per-thread direct ByteBuffer cache that
29+ // FileChannel.read() populates when reading into a heap buffer. That cache
30+ // retains the largest direct buffer ever requested — under chunked MEMORY
31+ // mode that would mean chunkSize bytes of off-heap memory held per loader
32+ // thread for the JVM's lifetime.
33+ try (FileInputStream stream = new FileInputStream (database )) {
34+ long size = database .length ();
35+ var name = database .getName ();
3036 if (size <= chunkSize ) {
31- // Allocate, read, and make read-only
32- ByteBuffer buffer = ByteBuffer .allocate ((int ) size );
33- if (channel .read (buffer ) != size ) {
34- throw new IOException ("Unable to read "
35- + database .getName ()
36- + " into memory. Unexpected end of stream." );
37- }
38- buffer .flip ();
39- this .buffer = new SingleBuffer (buffer );
37+ this .buffer = SingleBuffer .wrap (readFully (stream , (int ) size , name ));
4038 } else {
41- // Allocate chunks, read, and make read-only
4239 var fullChunks = (int ) (size / chunkSize );
4340 var remainder = (int ) (size % chunkSize );
4441 var totalChunks = fullChunks + (remainder > 0 ? 1 : 0 );
4542 var buffers = new ByteBuffer [totalChunks ];
46-
4743 for (int i = 0 ; i < fullChunks ; i ++) {
48- buffers [i ] = ByteBuffer .allocate ( chunkSize );
44+ buffers [i ] = ByteBuffer .wrap ( readFully ( stream , chunkSize , name ) );
4945 }
5046 if (remainder > 0 ) {
51- buffers [totalChunks - 1 ] = ByteBuffer .allocate (remainder );
47+ buffers [totalChunks - 1 ] = ByteBuffer .wrap (
48+ readFully (stream , remainder , name ));
5249 }
53-
54- var totalRead = 0L ;
55- for (var buffer : buffers ) {
56- var read = channel .read (buffer );
57- if (read == -1 ) {
58- break ;
59- }
60- totalRead += read ;
61- buffer .flip ();
62- }
63-
64- if (totalRead != size ) {
65- throw new IOException ("Unable to read "
66- + database .getName ()
67- + " into memory. Unexpected end of stream." );
68- }
69-
7050 this .buffer = new MultiBuffer (buffers , chunkSize );
7151 }
72- } else {
52+ }
53+ } else {
54+ try (RandomAccessFile file = new RandomAccessFile (database , "r" );
55+ FileChannel channel = file .getChannel ()) {
56+ long size = channel .size ();
7357 if (size <= chunkSize ) {
7458 this .buffer = SingleBuffer .mapFromChannel (channel );
7559 } else {
@@ -79,11 +63,32 @@ final class BufferHolder {
7963 }
8064 }
8165
82- BufferHolder (InputStream stream , int chunkSize ) throws IOException {
66+ BufferHolder (InputStream stream , int chunkSize ) throws IOException {
8367 if (null == stream ) {
8468 throw new NullPointerException ("Unable to use a NULL InputStream" );
8569 }
70+ this .buffer = readFromStream (stream , chunkSize );
71+ }
72+
73+ // Pre-allocates exactly len bytes. Used by file-backed MEMORY mode where the size is
74+ // known up front, avoiding the transient peak from ByteArrayOutputStream.grow() and
75+ // the defensive copy in toByteArray().
76+ private static byte [] readFully (InputStream stream , int len , String name ) throws IOException {
77+ var data = new byte [len ];
78+ var totalRead = 0 ;
79+ while (totalRead < len ) {
80+ var n = stream .read (data , totalRead , len - totalRead );
81+ if (n < 0 ) {
82+ throw new IOException ("Unable to read "
83+ + name
84+ + " into memory. Unexpected end of stream." );
85+ }
86+ totalRead += n ;
87+ }
88+ return data ;
89+ }
8690
91+ private static Buffer readFromStream (InputStream stream , int chunkSize ) throws IOException {
8792 // Read data from the stream in chunks to support databases >2GB.
8893 // Invariant: All chunks except the last are exactly chunkSize bytes.
8994 var chunks = new ArrayList <byte []>();
@@ -116,17 +121,16 @@ final class BufferHolder {
116121
117122 if (chunks .size () == 1 ) {
118123 // For databases that fit in a single chunk, use SingleBuffer
119- this .buffer = SingleBuffer .wrap (chunks .get (0 ));
120- } else {
121- // For large databases, wrap chunks in ByteBuffers and use MultiBuffer
122- // Guaranteed: chunks[0..n-2] all have length == chunkSize
123- // chunks[n-1] may have length < chunkSize
124- var buffers = new ByteBuffer [chunks .size ()];
125- for (var i = 0 ; i < chunks .size (); i ++) {
126- buffers [i ] = ByteBuffer .wrap (chunks .get (i ));
127- }
128- this .buffer = new MultiBuffer (buffers , chunkSize );
124+ return SingleBuffer .wrap (chunks .get (0 ));
125+ }
126+ // For large databases, wrap chunks in ByteBuffers and use MultiBuffer
127+ // Guaranteed: chunks[0..n-2] all have length == chunkSize
128+ // chunks[n-1] may have length < chunkSize
129+ var buffers = new ByteBuffer [chunks .size ()];
130+ for (var i = 0 ; i < chunks .size (); i ++) {
131+ buffers [i ] = ByteBuffer .wrap (chunks .get (i ));
129132 }
133+ return new MultiBuffer (buffers , chunkSize );
130134 }
131135
132136 /*
0 commit comments