/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2003-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.resources;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.geotools.util.logging.Logging;
/**
* Utility class for managing memory mapped buffers.
*
* @since 2.0
*
* @source $URL$
* @version $Id$
* @author Andrea Aimes
*/
public final class NIOUtilities {
/**
* The buffer cache, partitioned by buffer size and fully concurrent
*/
static Map<Integer, Queue<Object>> cache = new ConcurrentHashMap<Integer, Queue<Object>>();
static Map<Class, Method> cleanerMethodCache = new ConcurrentHashMap<Class, Method>();
/**
* The maximum size of the hard reference cache (the soft one can be unbounded, the GC will
* regulate its size according to the memory pressure)
*/
static int maxCacheSize = 2 * 1024 * 1024;
/**
* The current hard cache size
*/
static AtomicInteger hardCacheSize = new AtomicInteger(0);
/**
* A zero filled byte used to quickly reset the byte buffers
*/
static final byte[] ZEROES = new byte[4096];
/**
* {@code true} if a warning has already been logged.
*/
private static boolean warned = false;
/**
* Wheter direct buffers usage is enabled, or not
*/
private static boolean directBuffersEnabled = true;
static {
String directBuffers = System.getProperty("geotools.nioutilities.direct", "true");
directBuffersEnabled = "TRUE".equalsIgnoreCase(directBuffers);
}
/**
* Wheter direct buffers are used, or not (defaults to true)
* @return
*/
public static boolean isDirectBuffersEnabled() {
return directBuffersEnabled;
}
/**
* If the flag is true {@link #allocate(int)} will allocate a direct buffer,, otherwise
* heap buffers will be used. Direct buffers are normally faster, but their cleanup is
* platform dependent and not guaranteed, under high load and in combination with some
* garbage collectors that might result in a JVM crash (failure to perform native memory allocation)
*
* @param directBuffersEnabled
*/
public static void setDirectBuffersEnabled(boolean directBuffersEnabled) {
NIOUtilities.directBuffersEnabled = directBuffersEnabled;
}
/**
* Do not allows instantiation of this class.
*/
private NIOUtilities() {
}
/**
* Sets the maximum byte buffer cache size, in bytes (set to 0 to only use soft references
* in the case, a positive value will make the cache use hard references up to the max cache
* size)
* @param maxCacheSize
*/
public static void setMaxCacheSize(int maxCacheSize) {
NIOUtilities.maxCacheSize = maxCacheSize;
}
/**
* Allocates and returns a {@link ByteBuffer}. The buffer capacity will generally be greater than
* of two that can contain the specified limit, the buffer limit will be set at the specified
* value. The buffers are pooled, so remember to call {@link #clean(ByteBuffer, false)} to return
* the buffer to the pool.
*
* @param limit
* @return
*/
public static ByteBuffer allocate(int size) {
// look for a free cached buffer that has still not been garbage collected
Queue<Object> buffers = getBuffers(size);
Object sr = null;
while ((sr = buffers.poll()) != null) {
ByteBuffer buffer = null;
// what did we get, a soft or a hard reference?
if(sr instanceof BufferSoftReference) {
buffer = ((BufferSoftReference)sr).get();
} else {
// we're removing a hard reference from the cache, lower the usage figure
buffer = (ByteBuffer) sr;
hardCacheSize.addAndGet(-buffer.capacity());
}
// clean up the buffer and return it
if (buffer != null) {
buffer.clear();
return buffer;
}
}
// we could not find one, then allocated it
if(directBuffersEnabled) {
return ByteBuffer.allocateDirect(size);
} else {
return ByteBuffer.allocate(size);
}
}
/**
* Returns the buffer queue associated to the specified size
* @param size
* @return
*/
private static Queue<Object> getBuffers(int size) {
Queue<Object> result = cache.get(size);
if (result == null) {
// this is the only synchronized bit, we don't want multiple queues
// to be created. result == null will be true only at the application startup
// for the common byte buffer sizes
synchronized (cache) {
result = cache.get(size);
if (result == null) {
result = new ConcurrentLinkedQueue<Object>();
cache.put(size, result);
}
}
}
return result;
}
/**
* Depending on the type of buffer different cleanup action will be taken:
* <ul><li>if the buffer is memory mapped (as per the specified parameter) the effect is the same as {@link #clean(ByteBuffer)}</li>
* <li>if the buffer is not memory mapped it will be returned to the buffer cache</li>
* </ul>
* @param buffer
* @return
*/
public static boolean clean(final ByteBuffer buffer, boolean memoryMapped) {
if(memoryMapped) {
return clean(buffer);
} else {
if(returnToCache(buffer)) {
return true;
} else {
return clean(buffer);
}
}
}
/**
* Really closes a {@code MappedByteBuffer} without the need to wait for garbage collection. Any
* problems with closing a buffer on Windows (the problem child in this case) will be logged as
* {@code SEVERE} to the logger of the package name. To force logging of errors, set the System
* property "org.geotools.io.debugBuffer" to "true".
*
* @param buffer The buffer to close.
*
* @return true if the operation was successful, false otherwise.
*
* @see java.nio.MappedByteBuffer
*/
public static boolean clean(final ByteBuffer buffer) {
if(buffer == null || !buffer.isDirect()) {
return true;
}
Boolean b = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
public Boolean run() {
Boolean success = Boolean.FALSE;
try {
Method getCleanerMethod = getCleanerMethod(buffer);
if(getCleanerMethod != null) {
Object cleaner = getCleanerMethod.invoke(buffer, (Object[]) null);
if(cleaner != null) {
Method clean = cleaner.getClass().getMethod("clean", (Class[]) null);
clean.invoke(cleaner, (Object[]) null);
success = Boolean.TRUE;
}
}
} catch (Exception e) {
// This really is a show stopper on windows
if (isLoggable()) {
log(e, buffer);
}
}
return success;
}
});
return b.booleanValue();
}
static Method getCleanerMethod(final ByteBuffer buffer) throws NoSuchMethodException {
Method result = cleanerMethodCache.get(buffer.getClass());
if(result == null) {
result = buffer.getClass().getMethod("cleaner", (Class[]) null);
result.setAccessible(true);
cleanerMethodCache.put(buffer.getClass(), result);
}
return result;
}
public static boolean returnToCache(final ByteBuffer buffer) {
// is the buffer cacheable? There are some blessed sizes we use over and over, for the
// rest buffer only powers of two
final int capacity = buffer.capacity();
if(capacity != 100 && capacity != 13 && capacity != 16000) {
int size = (int) Math.pow(2, Math.ceil(Math.log(capacity) / Math.log(2)));
if(size != capacity) {
return false;
}
}
// clean up the buffer -> we need to zero out its contents as if it was just
// created or some shapefile tests will start failing
buffer.clear();
buffer.order(ByteOrder.BIG_ENDIAN);
// set the buffer back in the cache, either as a soft reference or as
// a hard one depending on whether we're past the hard cache or not
Queue<Object> buffers = cache.get(capacity);
if(hardCacheSize.get() > maxCacheSize) {
buffers.add(new BufferSoftReference(buffer));
} else {
hardCacheSize.addAndGet(capacity);
buffers.add(buffer);
}
return true;
}
/**
* Checks if a warning message should be logged.
*/
private static synchronized boolean isLoggable() {
try {
return !warned && (
Boolean.getBoolean("org.geotools.io.debugBuffer") ||
System.getProperty("os.name").indexOf("Windows") >= 0 );
} catch (SecurityException exception) {
// The utilities may be running in an Applet, in which case we
// can't read properties. Assumes we are not in debugging mode.
return false;
}
}
/**
* Logs a warning message.
*/
private static synchronized void log(final Exception e, final ByteBuffer buffer) {
warned = true;
String message = "Error attempting to close a mapped byte buffer : " + buffer.getClass().getName()
+ "\n JVM : " + System.getProperty("java.version")
+ ' ' + System.getProperty("java.vendor");
Logging.getLogger("org.geotools.io").log(Level.SEVERE, message, e);
}
}