1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.store.fs; |
7 | |
8 | import java.io.EOFException; |
9 | import java.io.IOException; |
10 | import java.io.RandomAccessFile; |
11 | import java.lang.ref.WeakReference; |
12 | import java.lang.reflect.Method; |
13 | import java.nio.BufferUnderflowException; |
14 | import java.nio.ByteBuffer; |
15 | import java.nio.MappedByteBuffer; |
16 | import java.nio.channels.FileChannel; |
17 | import java.nio.channels.FileLock; |
18 | import java.nio.channels.NonWritableChannelException; |
19 | |
20 | import org.h2.engine.SysProperties; |
21 | |
22 | /** |
23 | * This file system stores files on disk and uses java.nio to access the files. |
24 | * This class used memory mapped files. |
25 | */ |
26 | public class FilePathNioMapped extends FilePathNio { |
27 | |
28 | @Override |
29 | public FileChannel open(String mode) throws IOException { |
30 | return new FileNioMapped(name.substring(getScheme().length() + 1), mode); |
31 | } |
32 | |
33 | @Override |
34 | public String getScheme() { |
35 | return "nioMapped"; |
36 | } |
37 | |
38 | } |
39 | |
40 | /** |
41 | * Uses memory mapped files. |
42 | * The file size is limited to 2 GB. |
43 | */ |
44 | class FileNioMapped extends FileBase { |
45 | |
46 | private static final long GC_TIMEOUT_MS = 10000; |
47 | private final String name; |
48 | private final MapMode mode; |
49 | private RandomAccessFile file; |
50 | private MappedByteBuffer mapped; |
51 | private long fileLength; |
52 | |
53 | /** |
54 | * The position within the file. Can't use the position of the mapped buffer |
55 | * because it doesn't support seeking past the end of the file. |
56 | */ |
57 | private int pos; |
58 | |
59 | FileNioMapped(String fileName, String mode) throws IOException { |
60 | if ("r".equals(mode)) { |
61 | this.mode = MapMode.READ_ONLY; |
62 | } else { |
63 | this.mode = MapMode.READ_WRITE; |
64 | } |
65 | this.name = fileName; |
66 | file = new RandomAccessFile(fileName, mode); |
67 | reMap(); |
68 | } |
69 | |
70 | private void unMap() throws IOException { |
71 | if (mapped == null) { |
72 | return; |
73 | } |
74 | // first write all data |
75 | mapped.force(); |
76 | |
77 | // need to dispose old direct buffer, see bug |
78 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038 |
79 | |
80 | boolean useSystemGc = true; |
81 | if (SysProperties.NIO_CLEANER_HACK) { |
82 | try { |
83 | Method cleanerMethod = mapped.getClass().getMethod("cleaner"); |
84 | cleanerMethod.setAccessible(true); |
85 | Object cleaner = cleanerMethod.invoke(mapped); |
86 | if (cleaner != null) { |
87 | Method clearMethod = cleaner.getClass().getMethod("clean"); |
88 | clearMethod.invoke(cleaner); |
89 | } |
90 | useSystemGc = false; |
91 | } catch (Throwable e) { |
92 | // useSystemGc is already true |
93 | } finally { |
94 | mapped = null; |
95 | } |
96 | } |
97 | if (useSystemGc) { |
98 | WeakReference<MappedByteBuffer> bufferWeakRef = |
99 | new WeakReference<MappedByteBuffer>(mapped); |
100 | mapped = null; |
101 | long start = System.currentTimeMillis(); |
102 | while (bufferWeakRef.get() != null) { |
103 | if (System.currentTimeMillis() - start > GC_TIMEOUT_MS) { |
104 | throw new IOException("Timeout (" + GC_TIMEOUT_MS |
105 | + " ms) reached while trying to GC mapped buffer"); |
106 | } |
107 | System.gc(); |
108 | Thread.yield(); |
109 | } |
110 | } |
111 | } |
112 | |
113 | /** |
114 | * Re-map byte buffer into memory, called when file size has changed or file |
115 | * was created. |
116 | */ |
117 | private void reMap() throws IOException { |
118 | int oldPos = 0; |
119 | if (mapped != null) { |
120 | oldPos = pos; |
121 | unMap(); |
122 | } |
123 | fileLength = file.length(); |
124 | checkFileSizeLimit(fileLength); |
125 | // maps new MappedByteBuffer; the old one is disposed during GC |
126 | mapped = file.getChannel().map(mode, 0, fileLength); |
127 | int limit = mapped.limit(); |
128 | int capacity = mapped.capacity(); |
129 | if (limit < fileLength || capacity < fileLength) { |
130 | throw new IOException("Unable to map: length=" + limit + |
131 | " capacity=" + capacity + " length=" + fileLength); |
132 | } |
133 | if (SysProperties.NIO_LOAD_MAPPED) { |
134 | mapped.load(); |
135 | } |
136 | this.pos = Math.min(oldPos, (int) fileLength); |
137 | } |
138 | |
139 | private static void checkFileSizeLimit(long length) throws IOException { |
140 | if (length > Integer.MAX_VALUE) { |
141 | throw new IOException( |
142 | "File over 2GB is not supported yet when using this file system"); |
143 | } |
144 | } |
145 | |
146 | @Override |
147 | public void implCloseChannel() throws IOException { |
148 | if (file != null) { |
149 | unMap(); |
150 | file.close(); |
151 | file = null; |
152 | } |
153 | } |
154 | |
155 | @Override |
156 | public long position() { |
157 | return pos; |
158 | } |
159 | |
160 | @Override |
161 | public String toString() { |
162 | return "nioMapped:" + name; |
163 | } |
164 | |
165 | @Override |
166 | public synchronized long size() throws IOException { |
167 | return fileLength; |
168 | } |
169 | |
170 | @Override |
171 | public synchronized int read(ByteBuffer dst) throws IOException { |
172 | try { |
173 | int len = dst.remaining(); |
174 | if (len == 0) { |
175 | return 0; |
176 | } |
177 | len = (int) Math.min(len, fileLength - pos); |
178 | if (len <= 0) { |
179 | return -1; |
180 | } |
181 | mapped.position(pos); |
182 | mapped.get(dst.array(), dst.arrayOffset() + dst.position(), len); |
183 | dst.position(dst.position() + len); |
184 | pos += len; |
185 | return len; |
186 | } catch (IllegalArgumentException e) { |
187 | EOFException e2 = new EOFException("EOF"); |
188 | e2.initCause(e); |
189 | throw e2; |
190 | } catch (BufferUnderflowException e) { |
191 | EOFException e2 = new EOFException("EOF"); |
192 | e2.initCause(e); |
193 | throw e2; |
194 | } |
195 | } |
196 | |
197 | @Override |
198 | public FileChannel position(long pos) throws IOException { |
199 | checkFileSizeLimit(pos); |
200 | this.pos = (int) pos; |
201 | return this; |
202 | } |
203 | |
204 | @Override |
205 | public synchronized FileChannel truncate(long newLength) throws IOException { |
206 | // compatibility with JDK FileChannel#truncate |
207 | if (mode == MapMode.READ_ONLY) { |
208 | throw new NonWritableChannelException(); |
209 | } |
210 | if (newLength < size()) { |
211 | setFileLength(newLength); |
212 | } |
213 | return this; |
214 | } |
215 | |
216 | public synchronized void setFileLength(long newLength) throws IOException { |
217 | checkFileSizeLimit(newLength); |
218 | int oldPos = pos; |
219 | unMap(); |
220 | for (int i = 0;; i++) { |
221 | try { |
222 | file.setLength(newLength); |
223 | break; |
224 | } catch (IOException e) { |
225 | if (i > 16 || e.toString().indexOf("user-mapped section open") < 0) { |
226 | throw e; |
227 | } |
228 | } |
229 | System.gc(); |
230 | } |
231 | reMap(); |
232 | pos = (int) Math.min(newLength, oldPos); |
233 | } |
234 | |
235 | @Override |
236 | public void force(boolean metaData) throws IOException { |
237 | mapped.force(); |
238 | file.getFD().sync(); |
239 | } |
240 | |
241 | @Override |
242 | public synchronized int write(ByteBuffer src) throws IOException { |
243 | int len = src.remaining(); |
244 | // check if need to expand file |
245 | if (mapped.capacity() < pos + len) { |
246 | setFileLength(pos + len); |
247 | } |
248 | mapped.position(pos); |
249 | mapped.put(src); |
250 | pos += len; |
251 | return len; |
252 | } |
253 | |
254 | @Override |
255 | public synchronized FileLock tryLock(long position, long size, |
256 | boolean shared) throws IOException { |
257 | return file.getChannel().tryLock(position, size, shared); |
258 | } |
259 | |
260 | } |