Skip to content

Commit 9ea6e89

Browse files
committed
Clean ByteBuffer correctly in java 9
DEVSIX-1671
1 parent 587e405 commit 9ea6e89

File tree

1 file changed

+123
-9
lines changed

1 file changed

+123
-9
lines changed

io/src/main/java/com/itextpdf/io/source/ByteBufferRandomAccessSource.java

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@ This file is part of the iText (R) project.
5151
import java.io.ObjectInputStream;
5252
import java.io.ObjectOutputStream;
5353
import java.io.Serializable;
54+
import java.lang.invoke.MethodHandle;
5455
import java.lang.reflect.Method;
5556
import java.nio.BufferUnderflowException;
5657
import java.security.AccessController;
5758
import java.security.PrivilegedAction;
5859

60+
import java.nio.ByteBuffer;
61+
import java.util.Objects;
62+
import java.lang.reflect.Field;
63+
64+
import static java.lang.invoke.MethodHandles.Lookup;
65+
import static java.lang.invoke.MethodHandles.lookup;
66+
import static java.lang.invoke.MethodType.methodType;
67+
68+
5969
/**
6070
* A RandomAccessSource that is based on an underlying {@link java.nio.ByteBuffer}. This class takes steps to ensure that the byte buffer
6171
* is completely freed from memory during {@link ByteBufferRandomAccessSource#close()}
@@ -71,6 +81,7 @@ class ByteBufferRandomAccessSource implements IRandomAccessSource, Serializable
7181

7282
/**
7383
* Constructs a new {@link ByteBufferRandomAccessSource} based on the specified ByteBuffer
84+
*
7485
* @param byteBuffer the buffer to use as the backing store
7586
*/
7687
public ByteBufferRandomAccessSource(java.nio.ByteBuffer byteBuffer) {
@@ -81,6 +92,7 @@ public ByteBufferRandomAccessSource(java.nio.ByteBuffer byteBuffer) {
8192
* {@inheritDoc}
8293
* <p>
8394
* Note: Because ByteBuffers don't support long indexing, the position must be a valid positive int
95+
*
8496
* @param position the position to read the byte from - must be less than Integer.MAX_VALUE
8597
*/
8698
public int get(long position) throws java.io.IOException {
@@ -90,7 +102,7 @@ public int get(long position) throws java.io.IOException {
90102

91103
if (position >= byteBuffer.limit())
92104
return -1;
93-
byte b = byteBuffer.get((int)position);
105+
byte b = byteBuffer.get((int) position);
94106
return b & 0xff;
95107
} catch (BufferUnderflowException e) {
96108
return -1; // EOF
@@ -101,6 +113,7 @@ public int get(long position) throws java.io.IOException {
101113
* {@inheritDoc}
102114
* <p>
103115
* Note: Because ByteBuffers don't support long indexing, the position must be a valid positive int
116+
*
104117
* @param position the position to read the byte from - must be less than Integer.MAX_VALUE
105118
*/
106119
public int get(long position, byte[] bytes, int off, int len) throws java.io.IOException {
@@ -111,7 +124,7 @@ public int get(long position, byte[] bytes, int off, int len) throws java.io.IOE
111124
return -1;
112125

113126
// Not thread safe!
114-
byteBuffer.position((int)position);
127+
byteBuffer.position((int) position);
115128
int bytesFromThisBuffer = Math.min(len, byteBuffer.remaining());
116129
byteBuffer.get(bytes, off, bytesFromThisBuffer);
117130

@@ -134,8 +147,35 @@ public void close() throws java.io.IOException {
134147
clean(byteBuffer);
135148
}
136149

150+
151+
/**
152+
* <code>true</code>, if this platform supports unmapping mmapped files.
153+
*/
154+
public static final boolean UNMAP_SUPPORTED;
155+
156+
/**
157+
* Reference to a BufferCleaner that does unmapping; {@code null} if not supported.
158+
*/
159+
private static final BufferCleaner CLEANER;
160+
161+
static {
162+
final Object hack = AccessController.doPrivileged(new PrivilegedAction<Object>() {
163+
public Object run() {
164+
return unmapHackImpl();
165+
}
166+
});
167+
if (hack instanceof BufferCleaner) {
168+
CLEANER = (BufferCleaner) hack;
169+
UNMAP_SUPPORTED = true;
170+
} else {
171+
CLEANER = null;
172+
UNMAP_SUPPORTED = false;
173+
}
174+
}
175+
137176
/**
138177
* invokes the clean method on the ByteBuffer's cleaner
178+
*
139179
* @param buffer ByteBuffer
140180
* @return boolean true on success
141181
*/
@@ -147,11 +187,17 @@ private static boolean clean(final java.nio.ByteBuffer buffer) {
147187
public Boolean run() {
148188
Boolean success = Boolean.FALSE;
149189
try {
150-
Method getCleanerMethod = buffer.getClass().getMethod("cleaner", (Class<?>[]) null);
151-
getCleanerMethod.setAccessible(true);
152-
Object cleaner = getCleanerMethod.invoke(buffer, (Object[]) null);
153-
Method clean = cleaner.getClass().getMethod("clean", (Class<?>[]) null);
154-
clean.invoke(cleaner, (Object[]) null);
190+
// java 9
191+
if (UNMAP_SUPPORTED)
192+
CLEANER.freeBuffer(buffer.toString(), buffer);
193+
// java 8 and lower
194+
else {
195+
Method getCleanerMethod = buffer.getClass().getMethod("cleaner", (Class<?>[]) null);
196+
getCleanerMethod.setAccessible(true);
197+
Object cleaner = getCleanerMethod.invoke(buffer, (Object[]) null);
198+
Method clean = cleaner.getClass().getMethod("clean", (Class<?>[]) null);
199+
clean.invoke(cleaner, (Object[]) null);
200+
}
155201
success = Boolean.TRUE;
156202
} catch (Exception e) {
157203
// This really is a show stopper on windows
@@ -174,12 +220,80 @@ private void writeObject(ObjectOutputStream out) throws IOException {
174220
out.defaultWriteObject();
175221
}
176222

177-
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
178-
{
223+
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
179224
in.defaultReadObject();
180225
if (bufferMirror != null) {
181226
byteBuffer = java.nio.ByteBuffer.wrap(bufferMirror);
182227
bufferMirror = null;
183228
}
184229
}
230+
231+
/*
232+
* Licensed to the Apache Software Foundation (ASF) under one or more
233+
* contributor license agreements. See the NOTICE file distributed with
234+
* this work for additional information regarding copyright ownership.
235+
* The ASF licenses this file to You under the Apache License, Version 2.0
236+
* (the "License"); you may not use this file except in compliance with
237+
* the License. You may obtain a copy of the License at
238+
*
239+
* http://www.apache.org/licenses/LICENSE-2.0
240+
*
241+
* Unless required by applicable law or agreed to in writing, software
242+
* distributed under the License is distributed on an "AS IS" BASIS,
243+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
244+
* See the License for the specific language governing permissions and
245+
* limitations under the License.
246+
*/
247+
248+
private static class BufferCleaner {
249+
Class<?> unmappableBufferClass;
250+
final MethodHandle unmapper;
251+
252+
BufferCleaner(final Class<?> unmappableBufferClass, final MethodHandle unmapper) {
253+
this.unmappableBufferClass = unmappableBufferClass;
254+
this.unmapper = unmapper;
255+
}
256+
257+
void freeBuffer(String resourceDescription, final ByteBuffer buffer) throws IOException {
258+
assert Objects.equals(methodType(void.class, ByteBuffer.class), unmapper.type());
259+
if (!buffer.isDirect()) {
260+
throw new IllegalArgumentException("unmapping only works with direct buffers");
261+
}
262+
if (!unmappableBufferClass.isInstance(buffer)) {
263+
throw new IllegalArgumentException("buffer is not an instance of " + unmappableBufferClass.getName());
264+
}
265+
final Throwable error = AccessController.doPrivileged(new PrivilegedAction<Throwable>() {
266+
public Throwable run() {
267+
try {
268+
unmapper.invokeExact(buffer);
269+
return null;
270+
} catch (Throwable t) {
271+
return t;
272+
}
273+
}
274+
});
275+
if (error != null) {
276+
throw new IOException("Unable to unmap the mapped buffer: " + resourceDescription, error);
277+
}
278+
}
279+
}
280+
281+
private static Object unmapHackImpl() {
282+
final Lookup lookup = lookup();
283+
try {
284+
// *** sun.misc.Unsafe unmapping (Java 9+) ***
285+
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
286+
// first check if Unsafe has the right method, otherwise we can give up
287+
// without doing any security critical stuff:
288+
final MethodHandle unmapper = lookup.findVirtual(unsafeClass, "invokeCleaner",
289+
methodType(void.class, ByteBuffer.class));
290+
// fetch the unsafe instance and bind it to the virtual MH:
291+
final Field f = unsafeClass.getDeclaredField("theUnsafe");
292+
f.setAccessible(true);
293+
final Object theUnsafe = f.get(null);
294+
return new BufferCleaner(ByteBuffer.class, unmapper.bindTo(theUnsafe));
295+
} catch (Exception e) {
296+
return e.getMessage();
297+
}
298+
}
185299
}

0 commit comments

Comments
 (0)