From 74eb608be6e926bc77bb60b7279b08f9c67c691e Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 12 Aug 2010 17:01:49 +0000 Subject: [PATCH] HHH-5300 - Configurable strong and soft reference QueryPlanCache sizes git-svn-id: https://svn.jboss.org/repos/hibernate/core/trunk@20135 1b8cb986-b30d-0410-93ca-fae66ebed9b2 --- .../java/org/hibernate/cfg/Environment.java | 11 + .../engine/query/QueryPlanCache.java | 98 +++++---- .../hibernate/impl/SessionFactoryImpl.java | 3 +- .../main/java/org/hibernate/util/LRUMap.java | 50 +++++ .../org/hibernate/util/SoftLimitMRUCache.java | 194 ++++++++++++++---- 5 files changed, 285 insertions(+), 71 deletions(-) create mode 100644 core/src/main/java/org/hibernate/util/LRUMap.java diff --git a/core/src/main/java/org/hibernate/cfg/Environment.java b/core/src/main/java/org/hibernate/cfg/Environment.java index 0814b271f0..4fd6621caa 100644 --- a/core/src/main/java/org/hibernate/cfg/Environment.java +++ b/core/src/main/java/org/hibernate/cfg/Environment.java @@ -537,6 +537,17 @@ public final class Environment { */ public static final String PREFER_POOLED_VALUES_LO = "hibernate.id.optimizer.pooled.prefer_lo"; + /** + * The maximum number of strong references maintained by {@link org.hibernate.util.SoftLimitMRUCache}. Default is 128. + */ + public static final String QUERY_PLAN_CACHE_MAX_STRONG_REFERENCES = "hibernate.query.plan_cache_max_strong_references"; + + /** + * The maximum number of soft references maintained by {@link org.hibernate.util.SoftLimitMRUCache}. Default is 2048. + */ + public static final String QUERY_PLAN_CACHE_MAX_SOFT_REFERENCES = "hibernate.query.plan_cache_max_soft_references"; + + private static final BytecodeProvider BYTECODE_PROVIDER_INSTANCE; private static final boolean ENABLE_BINARY_STREAMS; private static final boolean ENABLE_REFLECTION_OPTIMIZER; diff --git a/core/src/main/java/org/hibernate/engine/query/QueryPlanCache.java b/core/src/main/java/org/hibernate/engine/query/QueryPlanCache.java index 462692dad4..7d3e0539b7 100644 --- a/core/src/main/java/org/hibernate/engine/query/QueryPlanCache.java +++ b/core/src/main/java/org/hibernate/engine/query/QueryPlanCache.java @@ -1,10 +1,10 @@ /* * Hibernate, Relational Persistence for Idiomatic Java * - * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as + * Copyright (c) 2010, Red Hat Inc. or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are - * distributed under license by Red Hat Middleware LLC. + * distributed under license by Red Hat Inc. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU @@ -20,13 +20,14 @@ * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA - * */ package org.hibernate.engine.query; +import org.hibernate.util.PropertiesHelper; import org.hibernate.util.SimpleMRUCache; import org.hibernate.util.SoftLimitMRUCache; import org.hibernate.util.CollectionHelper; +import org.hibernate.cfg.Environment; import org.hibernate.engine.SessionFactoryImplementor; import org.hibernate.engine.query.sql.NativeSQLQuerySpecification; import org.hibernate.QueryException; @@ -48,6 +49,9 @@ import java.util.Collection; /** * Acts as a cache for compiled query plans, as well as query-parameter metadata. * + * @see Environment#QUERY_PLAN_CACHE_MAX_STRONG_REFERENCES + * @see Environment#QUERY_PLAN_CACHE_MAX_SOFT_REFERENCES + * * @author Steve Ebersole */ public class QueryPlanCache implements Serializable { @@ -57,29 +61,51 @@ public class QueryPlanCache implements Serializable { private SessionFactoryImplementor factory; public QueryPlanCache(SessionFactoryImplementor factory) { + int maxStrongReferenceCount = PropertiesHelper.getInt( + Environment.QUERY_PLAN_CACHE_MAX_STRONG_REFERENCES, + factory.getProperties(), + SoftLimitMRUCache.DEFAULT_STRONG_REF_COUNT + ); + int maxSoftReferenceCount = PropertiesHelper.getInt( + Environment.QUERY_PLAN_CACHE_MAX_SOFT_REFERENCES, + factory.getProperties(), + SoftLimitMRUCache.DEFAULT_SOFT_REF_COUNT + ); + this.factory = factory; + this.sqlParamMetadataCache = new SimpleMRUCache( maxStrongReferenceCount ); + this.planCache = new SoftLimitMRUCache( maxStrongReferenceCount, maxSoftReferenceCount ); } - // simple cache of param metadata based on query string. Ideally, the - // original "user-supplied query" string should be used to retreive this - // metadata (i.e., not the para-list-expanded query string) to avoid - // unnecessary cache entries. - // Used solely for caching param metadata for native-sql queries, see - // getSQLParameterMetadata() for a discussion as to why... - private final SimpleMRUCache sqlParamMetadataCache = new SimpleMRUCache(); + /** + * simple cache of param metadata based on query string. Ideally, the original "user-supplied query" + * string should be used to obtain this metadata (i.e., not the para-list-expanded query string) to avoid + * unnecessary cache entries. + *

+ * Used solely for caching param metadata for native-sql queries, see {@link #getSQLParameterMetadata} for a + * discussion as to why... + */ + private final SimpleMRUCache sqlParamMetadataCache; - // the cache of the actual plans... - private final SoftLimitMRUCache planCache = new SoftLimitMRUCache( 128 ); + /** + * the cache of the actual plans... + */ + private final SoftLimitMRUCache planCache; + /** + * Obtain the parameter metadata for given native-sql query. + *

+ * for native-sql queries, the param metadata is determined outside any relation to a query plan, because + * query plan creation and/or retrieval for a native-sql query depends on all of the return types having been + * set, which might not be the case up-front when param metadata would be most useful + * + * @param query The query + * @return The parameter metadata + */ public ParameterMetadata getSQLParameterMetadata(String query) { ParameterMetadata metadata = ( ParameterMetadata ) sqlParamMetadataCache.get( query ); if ( metadata == null ) { - // for native-sql queries, the param metadata is determined outside - // any relation to a query plan, because query plan creation and/or - // retreival for a native-sql query depends on all of the return - // types having been set, which might not be the case up-front when - // param metadata would be most useful metadata = buildNativeSQLParameterMetadata( query ); sqlParamMetadataCache.put( query, metadata ); } @@ -149,6 +175,7 @@ public class QueryPlanCache implements Serializable { return plan; } + @SuppressWarnings({ "UnnecessaryUnboxing" }) private ParameterMetadata buildNativeSQLParameterMetadata(String sqlString) { ParamLocationRecognizer recognizer = ParamLocationRecognizer.parseLocations( sqlString ); @@ -160,7 +187,7 @@ public class QueryPlanCache implements Serializable { } Iterator itr = recognizer.getNamedParameterDescriptionMap().entrySet().iterator(); - Map namedParamDescriptorMap = new HashMap(); + Map namedParamDescriptorMap = new HashMap(); while( itr.hasNext() ) { final Map.Entry entry = ( Map.Entry ) itr.next(); final String name = ( String ) entry.getKey(); @@ -178,7 +205,7 @@ public class QueryPlanCache implements Serializable { private static class HQLQueryPlanKey implements Serializable { private final String query; private final boolean shallow; - private final Set filterKeys; + private final Set filterKeys; private final int hashCode; public HQLQueryPlanKey(String query, boolean shallow, Map enabledFilters) { @@ -186,16 +213,15 @@ public class QueryPlanCache implements Serializable { this.shallow = shallow; if ( enabledFilters == null || enabledFilters.isEmpty() ) { - filterKeys = Collections.EMPTY_SET; + filterKeys = Collections.emptySet(); } else { - Set tmp = new HashSet( + Set tmp = new HashSet( CollectionHelper.determineProperSizing( enabledFilters ), CollectionHelper.LOAD_FACTOR ); - Iterator itr = enabledFilters.values().iterator(); - while ( itr.hasNext() ) { - tmp.add( new DynamicFilterKey( ( FilterImpl ) itr.next() ) ); + for ( Object o : enabledFilters.values() ) { + tmp.add( new DynamicFilterKey( (FilterImpl) o ) ); } this.filterKeys = Collections.unmodifiableSet( tmp ); } @@ -229,30 +255,31 @@ public class QueryPlanCache implements Serializable { private static class DynamicFilterKey implements Serializable { private final String filterName; - private final Map parameterMetadata; + private final Map parameterMetadata; private final int hashCode; + @SuppressWarnings({ "UnnecessaryBoxing" }) private DynamicFilterKey(FilterImpl filter) { this.filterName = filter.getName(); if ( filter.getParameters().isEmpty() ) { - parameterMetadata = Collections.EMPTY_MAP; + parameterMetadata = Collections.emptyMap(); } else { - parameterMetadata = new HashMap( + parameterMetadata = new HashMap( CollectionHelper.determineProperSizing( filter.getParameters() ), CollectionHelper.LOAD_FACTOR ); - Iterator itr = filter.getParameters().entrySet().iterator(); - while ( itr.hasNext() ) { + for ( Object o : filter.getParameters().entrySet() ) { + final Map.Entry entry = (Map.Entry) o; + final String key = (String) entry.getKey(); final Integer valueCount; - final Map.Entry entry = ( Map.Entry ) itr.next(); if ( Collection.class.isInstance( entry.getValue() ) ) { valueCount = new Integer( ( (Collection) entry.getValue() ).size() ); } else { - valueCount = new Integer(1); + valueCount = new Integer( 1 ); } - parameterMetadata.put( entry.getKey(), valueCount ); + parameterMetadata.put( key, valueCount ); } } @@ -285,19 +312,20 @@ public class QueryPlanCache implements Serializable { private final String query; private final String collectionRole; private final boolean shallow; - private final Set filterNames; + private final Set filterNames; private final int hashCode; + @SuppressWarnings({ "unchecked" }) public FilterQueryPlanKey(String query, String collectionRole, boolean shallow, Map enabledFilters) { this.query = query; this.collectionRole = collectionRole; this.shallow = shallow; if ( enabledFilters == null || enabledFilters.isEmpty() ) { - filterNames = Collections.EMPTY_SET; + filterNames = Collections.emptySet(); } else { - Set tmp = new HashSet(); + Set tmp = new HashSet(); tmp.addAll( enabledFilters.keySet() ); this.filterNames = Collections.unmodifiableSet( tmp ); } diff --git a/core/src/main/java/org/hibernate/impl/SessionFactoryImpl.java b/core/src/main/java/org/hibernate/impl/SessionFactoryImpl.java index 67c51a00a5..fdf8531aba 100644 --- a/core/src/main/java/org/hibernate/impl/SessionFactoryImpl.java +++ b/core/src/main/java/org/hibernate/impl/SessionFactoryImpl.java @@ -185,7 +185,7 @@ public final class SessionFactoryImpl implements SessionFactory, SessionFactoryI private final transient SQLFunctionRegistry sqlFunctionRegistry; private final transient SessionFactoryObserver observer; private final transient HashMap entityNameResolvers = new HashMap(); - private final transient QueryPlanCache queryPlanCache = new QueryPlanCache( this ); + private final transient QueryPlanCache queryPlanCache; private final transient Cache cacheAccess = new CacheImpl(); private transient boolean isClosed = false; private final transient TypeResolver typeResolver; @@ -234,6 +234,7 @@ public final class SessionFactoryImpl implements SessionFactory, SessionFactoryI // Caches settings.getRegionFactory().start( settings, properties ); + this.queryPlanCache = new QueryPlanCache( this ); //Generators: diff --git a/core/src/main/java/org/hibernate/util/LRUMap.java b/core/src/main/java/org/hibernate/util/LRUMap.java new file mode 100644 index 0000000000..55170354a2 --- /dev/null +++ b/core/src/main/java/org/hibernate/util/LRUMap.java @@ -0,0 +1,50 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2010, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.util; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A simple LRU cache that implements the Map interface. Instances + * are not thread-safe and should be synchronized externally, for instance by + * using {@link java.util.Collections#synchronizedMap}. + * + * @author Manuel Dominguez Sarmiento + */ +public class LRUMap extends LinkedHashMap implements Serializable { + private static final long serialVersionUID = -5522608033020688048L; + + private final int maxEntries; + + public LRUMap(int maxEntries) { + super( maxEntries, .75f, true ); + this.maxEntries = maxEntries; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return ( size() > maxEntries ); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hibernate/util/SoftLimitMRUCache.java b/core/src/main/java/org/hibernate/util/SoftLimitMRUCache.java index 0087e2b06b..b423972e12 100644 --- a/core/src/main/java/org/hibernate/util/SoftLimitMRUCache.java +++ b/core/src/main/java/org/hibernate/util/SoftLimitMRUCache.java @@ -1,10 +1,10 @@ /* * Hibernate, Relational Persistence for Idiomatic Java * - * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as + * Copyright (c) 2010, Red Hat Inc. or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are - * distributed under license by Red Hat Middleware LLC. + * distributed under license by Red Hat Inc. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU @@ -20,18 +20,16 @@ * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA - * */ package org.hibernate.util; -import org.apache.commons.collections.map.ReferenceMap; -import org.apache.commons.collections.map.LRUMap; - import java.io.Serializable; import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; /** - * Cache following a "Most Recently Used" (MRY) algorithm for maintaining a + * Cache following a "Most Recently Used" (MRU) algorithm for maintaining a * bounded in-memory size; the "Least Recently Used" (LRU) entry is the first * available for removal from the cache. *

@@ -39,60 +37,168 @@ import java.io.IOException; * meaning that all cache entries are kept within a completely * {@link java.lang.ref.SoftReference}-based map with the most recently utilized * entries additionally kept in a hard-reference manner to prevent those cache - * entries soft references from becoming enqueued by the garbage collector. - * Thus the actual size of this cache impl can actually grow beyond the stated - * max size bound as long as GC is not actively seeking soft references for + * entries soft references from becoming enqueued by the garbage collector. Thus + * the actual size of this cache impl can actually grow beyond the stated max + * size bound as long as GC is not actively seeking soft references for * enqueuement. + *

+ * The soft-size is bounded and configurable. This allows controlling memory + * usage which can grow out of control under some circumstances, especially when + * very large heaps are in use. Although memory usage per se should not be a + * problem with soft references, which are cleared when necessary, this can + * trigger extremely slow stop-the-world GC pauses when nearing full heap usage, + * even with CMS concurrent GC (i.e. concurrent mode failure). This is most + * evident when ad-hoc HQL queries are produced by the application, leading to + * poor soft-cache hit ratios. This can also occur with heavy use of SQL IN + * clauses, which will generate multiples SQL queries (even if parameterized), + * one for each collection/array size passed to the IN clause. Many slightly + * different queries will eventually fill the heap and trigger a full GC to + * reclaim space, leading to unacceptable pauses in some cases. + *

+ * Note: This class is serializable, however all entries are + * discarded on serialization. + * + * @see org.hibernate.cfg.Environment#QUERY_PLAN_CACHE_MAX_STRONG_REFERENCES + * @see org.hibernate.cfg.Environment#QUERY_PLAN_CACHE_MAX_SOFT_REFERENCES * * @author Steve Ebersole + * @author Manuel Dominguez Sarmiento */ public class SoftLimitMRUCache implements Serializable { - + /** + * The default strong reference count. + */ public static final int DEFAULT_STRONG_REF_COUNT = 128; - private final int strongReferenceCount; + /** + * The default soft reference count. + */ + public static final int DEFAULT_SOFT_REF_COUNT = 2048; - // actual cache of the entries. soft references are used for both the keys and the - // values here since the values pertaining to the MRU entries are kept in a - // seperate hard reference cache (to avoid their enqueuement/garbage-collection). - private transient ReferenceMap softReferenceCache = new ReferenceMap( ReferenceMap.SOFT, ReferenceMap.SOFT ); - // the MRU cache used to keep hard references to the most recently used query plans; - // note : LRU here is a bit of a misnomer, it indicates that LRU entries are removed, the - // actual kept entries are the MRU entries - private transient LRUMap strongReferenceCache; + private final int strongRefCount; + private final int softRefCount; + private transient LRUMap strongRefCache; + private transient LRUMap softRefCache; + private transient ReferenceQueue referenceQueue; + + /** + * Constructs a cache with the default settings. + * + * @see #DEFAULT_STRONG_REF_COUNT + * @see #DEFAULT_SOFT_REF_COUNT + */ public SoftLimitMRUCache() { - this( DEFAULT_STRONG_REF_COUNT ); + this( DEFAULT_STRONG_REF_COUNT, DEFAULT_SOFT_REF_COUNT ); } - public SoftLimitMRUCache(int strongRefCount) { - this.strongReferenceCount = strongRefCount; + /** + * Constructs a cache with the specified settings. + * + * @param strongRefCount the strong reference count. + * @param softRefCount the soft reference count. + * + * @throws IllegalArgumentException if either of the arguments is less than one, or if the strong + * reference count is higher than the soft reference count. + */ + public SoftLimitMRUCache(int strongRefCount, int softRefCount) { + if ( strongRefCount < 1 || softRefCount < 1 ) { + throw new IllegalArgumentException( "Reference counts must be greater than zero" ); + } + if ( strongRefCount > softRefCount ) { + throw new IllegalArgumentException( "Strong reference count cannot exceed soft reference count" ); + } + + this.strongRefCount = strongRefCount; + this.softRefCount = softRefCount; init(); } + /** + * Gets an object from the cache. + * + * @param key the cache key. + * + * @return the stored value, or null if no entry exists. + */ public synchronized Object get(Object key) { - Object result = softReferenceCache.get( key ); - if ( result != null ) { - strongReferenceCache.put( key, result ); + if ( key == null ) { + throw new NullPointerException( "Key to get cannot be null" ); } - return result; + + clearObsoleteReferences(); + + SoftReference ref = (SoftReference) softRefCache.get( key ); + if ( ref != null ) { + Object refValue = ref.get(); + if ( refValue != null ) { + // This ensures recently used entries are strongly-reachable + strongRefCache.put( key, refValue ); + return refValue; + } + } + + return null; } + /** + * Puts a value in the cache. + * + * @param key the key. + * @param value the value. + * + * @return the previous value stored in the cache, if any. + */ public synchronized Object put(Object key, Object value) { - softReferenceCache.put( key, value ); - return strongReferenceCache.put( key, value ); + if ( key == null || value == null ) { + throw new NullPointerException( + getClass().getName() + "does not support null key [" + key + "] or value [" + value + "]" + ); + } + + clearObsoleteReferences(); + + strongRefCache.put( key, value ); + SoftReference ref = (SoftReference) softRefCache.put( + key, + new KeyedSoftReference( key, value, referenceQueue ) + ); + + return ( ref != null ) ? ref.get() : null; } + /** + * Gets the strong reference cache size. + * + * @return the strong reference cache size. + */ public synchronized int size() { - return strongReferenceCache.size(); + clearObsoleteReferences(); + return strongRefCache.size(); } + /** + * Gets the soft reference cache size. + * + * @return the soft reference cache size. + */ public synchronized int softSize() { - return softReferenceCache.size(); + clearObsoleteReferences(); + return softRefCache.size(); + } + + /** + * Clears the cache. + */ + public synchronized void clear() { + strongRefCache.clear(); + softRefCache.clear(); } private void init() { - strongReferenceCache = new LRUMap( strongReferenceCount ); + this.strongRefCache = new LRUMap( strongRefCount ); + this.softRefCache = new LRUMap( softRefCount ); + this.referenceQueue = new ReferenceQueue(); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { @@ -100,8 +206,26 @@ public class SoftLimitMRUCache implements Serializable { init(); } - public synchronized void clear() { - strongReferenceCache.clear(); - softReferenceCache.clear(); + private void clearObsoleteReferences() { + // Clear entries for soft references removed by garbage collector + KeyedSoftReference obsoleteRef; + while ( ( obsoleteRef = (KeyedSoftReference) referenceQueue.poll() ) != null ) { + Object key = obsoleteRef.getKey(); + softRefCache.remove( key ); + } + } + + private static class KeyedSoftReference extends SoftReference { + private final Object key; + + @SuppressWarnings({ "unchecked" }) + private KeyedSoftReference(Object key, Object value, ReferenceQueue q) { + super( value, q ); + this.key = key; + } + + private Object getKey() { + return key; + } } }