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,38 @@ 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+ // Size from the open fd (fstat) so it's atomic with the open;
35+ // getChannel().size() does not populate the buffer cache.
36+ long size = stream .getChannel ().size ();
37+ var name = database .getName ();
3038 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 );
39+ this .buffer = SingleBuffer .wrap (readFully (stream , (int ) size , name ));
4040 } else {
41- // Allocate chunks, read, and make read-only
4241 var fullChunks = (int ) (size / chunkSize );
4342 var remainder = (int ) (size % chunkSize );
4443 var totalChunks = fullChunks + (remainder > 0 ? 1 : 0 );
4544 var buffers = new ByteBuffer [totalChunks ];
46-
4745 for (int i = 0 ; i < fullChunks ; i ++) {
48- buffers [i ] = ByteBuffer .allocate ( chunkSize );
46+ buffers [i ] = ByteBuffer .wrap ( readFully ( stream , chunkSize , name ) );
4947 }
5048 if (remainder > 0 ) {
51- buffers [totalChunks - 1 ] = ByteBuffer .allocate (remainder );
49+ buffers [totalChunks - 1 ] = ByteBuffer .wrap (
50+ readFully (stream , remainder , name ));
5251 }
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-
7052 this .buffer = new MultiBuffer (buffers , chunkSize );
7153 }
72- } else {
54+ }
55+ } else {
56+ try (RandomAccessFile file = new RandomAccessFile (database , "r" );
57+ FileChannel channel = file .getChannel ()) {
58+ long size = channel .size ();
7359 if (size <= chunkSize ) {
7460 this .buffer = SingleBuffer .mapFromChannel (channel );
7561 } else {
@@ -79,11 +65,32 @@ final class BufferHolder {
7965 }
8066 }
8167
82- BufferHolder (InputStream stream , int chunkSize ) throws IOException {
68+ BufferHolder (InputStream stream , int chunkSize ) throws IOException {
8369 if (null == stream ) {
8470 throw new NullPointerException ("Unable to use a NULL InputStream" );
8571 }
72+ this .buffer = readFromStream (stream , chunkSize );
73+ }
74+
75+ // Pre-allocates exactly len bytes. Used by file-backed MEMORY mode where the size is
76+ // known up front, avoiding the transient peak from ByteArrayOutputStream.grow() and
77+ // the defensive copy in toByteArray().
78+ private static byte [] readFully (InputStream stream , int len , String name ) throws IOException {
79+ var data = new byte [len ];
80+ var totalRead = 0 ;
81+ while (totalRead < len ) {
82+ var n = stream .read (data , totalRead , len - totalRead );
83+ if (n < 0 ) {
84+ throw new IOException ("Unable to read "
85+ + name
86+ + " into memory. Unexpected end of stream." );
87+ }
88+ totalRead += n ;
89+ }
90+ return data ;
91+ }
8692
93+ private static Buffer readFromStream (InputStream stream , int chunkSize ) throws IOException {
8794 // Read data from the stream in chunks to support databases >2GB.
8895 // Invariant: All chunks except the last are exactly chunkSize bytes.
8996 var chunks = new ArrayList <byte []>();
@@ -116,17 +123,16 @@ final class BufferHolder {
116123
117124 if (chunks .size () == 1 ) {
118125 // 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 );
126+ return SingleBuffer .wrap (chunks .get (0 ));
127+ }
128+ // For large databases, wrap chunks in ByteBuffers and use MultiBuffer
129+ // Guaranteed: chunks[0..n-2] all have length == chunkSize
130+ // chunks[n-1] may have length < chunkSize
131+ var buffers = new ByteBuffer [chunks .size ()];
132+ for (var i = 0 ; i < chunks .size (); i ++) {
133+ buffers [i ] = ByteBuffer .wrap (chunks .get (i ));
129134 }
135+ return new MultiBuffer (buffers , chunkSize );
130136 }
131137
132138 /*
0 commit comments