Hi Luca,
we found another memory hole here. I know there is one left in the
index, but the first I found is in the cache.
Basically the problem is in java's LinkedHashMap. It does not release
the records at all! So heap is getting bigger and bigger all the time.
Below I send you some patch and a new implementation using google's
ConcurrentLinkedHashMap.
It is not completely finished, but should help you as a starting
point. In my tests (without index) everything works fine and we create
50 million records in a batch without needing more than 4 MB of heap.
Additionally I detach all records before I put it into the cache - I
think that should be to avoid references between the records.
Than I added a flag to avoid more than one memory watchdog listener
instance because you register it more than once.
Best regards,
Walter
1. add dependency:
==============
+ <dependency>
+ <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
+ <artifactId>concurrentlinkedhashmap-lru</artifactId>
+ <version>1.1</version>
+ </dependency>
2. the patch:
=========
ignore my logging output - but it helped to track down - especially
printing the size of cache before and after memorycleanup.
Index: src/main/java/com/orientechnologies/orient/core/cache/
OAbstractRecordCache.java
===================================================================
--- src/main/java/com/orientechnologies/orient/core/cache/
OAbstractRecordCache.java (revision 2506)
+++ src/main/java/com/orientechnologies/orient/core/cache/
OAbstractRecordCache.java (working copy)
@@ -22,6 +22,7 @@
import com.orientechnologies.orient.core.OMemoryWatchDog.Listener;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.id.ORID;
+import com.orientechnologies.orient.core.record.ORecordInternal;
/**
* Cache of documents.
@@ -31,23 +32,30 @@
*/
public abstract class OAbstractRecordCache extends OSharedResource {
protected int maxSize;
- protected ORecordCache entries;
+// protected ORecordCache entries;
+ protected GoogleRecordCache entries;
protected Listener watchDogListener;
protected String profilerPrefix;
+ protected String name;
+ protected boolean startupCompleted = false;
+
/**
* Create the cache of iMaxSize size.
*
* @param iMaxSize
* Maximum number of elements for the cache
*/
- public OAbstractRecordCache(final String iProfilerPrefix, final int
iMaxSize) {
+ public OAbstractRecordCache(final String iProfilerPrefix, final int
iMaxSize, final String name) {
profilerPrefix = iProfilerPrefix;
maxSize = iMaxSize;
+
this.name = name;
final int initialSize = maxSize > -1 ? maxSize + 1 : 1000;
- entries = new ORecordCache(maxSize, initialSize, 0.75f);
+// entries = new ORecordCache(maxSize, initialSize, 0.75f);
+ entries = new GoogleRecordCache(maxSize);
+
}
public boolean existsRecord(final ORID iRID) {
@@ -132,6 +140,7 @@
entries.clear();
Orient.instance().getMemoryWatchDog().removeListener(watchDogListener);
watchDogListener = null;
+ startupCompleted = false;
} finally {
releaseExclusiveLock();
@@ -143,13 +152,16 @@
}
public void startup() {
+ if (startupCompleted) return;
+
watchDogListener =
Orient.instance().getMemoryWatchDog().addListener(new Listener() {
/**
* Auto reduce cache size of 50%
*/
public void memoryUsageLow(TYPE iType, final long usedMemory,
final long maxMemory) {
if (iType == TYPE.JVM) {
- acquireExclusiveLock();
+ OLogManager.instance().info(this, "Low memory handler for cache
%s called", name + ":" + System.identityHashCode(this));
+ acquireExclusiveLock();
try {
final int oldSize = entries.size();
if (oldSize == 0)
@@ -157,11 +169,21 @@
return;
final int threshold = (int) (oldSize * 0.5f);
-
- entries.removeEldestItems(threshold);
-
- OLogManager.instance().debug(this, "Low memory: auto reduce the
record cache size from %d to %d", oldSize, threshold);
+
+ // reduce maxsize
+ if (maxSize > -1) {
+ maxSize = (int) (maxSize * 0.8f);
+ } else {
+ // if cache is unlimited, limit it now, because heap space is
running out -> automatically find right size
+ maxSize = (int) (oldSize * 0.8f);
+ }
+ entries.setMaxSize(maxSize);
+// entries.removeEldestItems(threshold);
+
+ OLogManager.instance().info(this, "----- reduced cache from %d
to %d entries, maxsize=%d", oldSize, entries.size(),
entries.getMaxSize());
+ OLogManager.instance().info(this, "Low memory: auto reduce the
record cache size from %d to %d", oldSize, threshold);
} catch (Exception e) {
+ e.printStackTrace();
OLogManager.instance().error(this, "Error while freeing
resources", e);
} finally {
releaseExclusiveLock();
@@ -202,5 +224,8 @@
return maxSize;
}
});
+
+ OLogManager.instance().info(this, "watchDogListener listener
created: %s:%s", this.toString(), System.identityHashCode(this));
+ startupCompleted = true;
}
}
Index: src/main/java/com/orientechnologies/orient/core/cache/
ODatabaseRecordCache.java
===================================================================
--- src/main/java/com/orientechnologies/orient/core/cache/
ODatabaseRecordCache.java (revision 2506)
+++ src/main/java/com/orientechnologies/orient/core/cache/
ODatabaseRecordCache.java (working copy)
@@ -37,7 +37,7 @@
private String PROFILER_CACHE_NOTFOUND;
public ODatabaseRecordCache(final ODatabaseRecord iDatabase) {
- super("db." + iDatabase.getName(),
OGlobalConfiguration.DB_CACHE_SIZE.getValueAsInteger());
+ super("db." + iDatabase.getName(),
OGlobalConfiguration.DB_CACHE_SIZE.getValueAsInteger(),
"ODatabaseRecordCache");
database = iDatabase;
}
@@ -59,6 +59,7 @@
acquireExclusiveLock();
try {
+ if (iRecord != null) iRecord.detach();
entries.put(iRecord.getIdentity(), iRecord);
} finally {
releaseExclusiveLock();
Index: src/main/java/com/orientechnologies/orient/core/cache/
OStorageRecordCache.java
===================================================================
--- src/main/java/com/orientechnologies/orient/core/cache/
OStorageRecordCache.java (revision 2506)
+++ src/main/java/com/orientechnologies/orient/core/cache/
OStorageRecordCache.java (working copy)
@@ -38,7 +38,7 @@
}
public OStorageRecordCache(final OStorage iStorage) {
- super("storage." + iStorage.getName(),
OGlobalConfiguration.STORAGE_CACHE_SIZE.getValueAsInteger());
+ super("storage." + iStorage.getName(),
OGlobalConfiguration.STORAGE_CACHE_SIZE.getValueAsInteger(),
"OStorageRecordCache");
storage = iStorage;
setStrategy(OGlobalConfiguration.STORAGE_CACHE_STRATEGY.getValueAsInteger());
}
@@ -51,8 +51,10 @@
acquireExclusiveLock();
try {
final ORecord<?> record = entries.get(iRecord.getIdentity());
- if (record == null || iRecord.getVersion() > record.getVersion())
+ if (record == null || iRecord.getVersion() > record.getVersion())
{
+ if (iRecord !=null) iRecord.detach();
entries.put(iRecord.getIdentity(), iRecord);
+ }
} finally {
releaseExclusiveLock();
------
3. in the end the code of my GoogleRecordCache:
====================================
package com.orientechnologies.orient.core.cache;
import com.googlecode.concurrentlinkedhashmap.CapacityLimiter;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.Weigher;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.record.ORecordInternal;
/**
* @author Walter Raboch <
wra...@ingen.at>
*
*/
public class GoogleRecordCache {
ConcurrentLinkedHashMap<ORID, ORecordInternal<?>> map;
/**
* @param maxSize
*/
public GoogleRecordCache(final int maxSize) {
// TODO make size configurable
map = new ConcurrentLinkedHashMap.Builder<ORID, ORecordInternal<?
>>()
.weigher(new Weigher<ORecordInternal<?>>() {
public int weightOf(ORecordInternal<?> arg0) {
return 1;
}
})
.maximumWeightedCapacity(10000)
.capacityLimiter(new CapacityLimiter() {
public boolean hasExceededCapacity(ConcurrentLinkedHashMap<?, ?>
map) {
if (map.size() > 10000)
return true;
return false;
}
})
.build();
}
public int getMaxSize() {
return map.capacity();
}
public void setMaxSize(final int maxSize) {
// TODO make size hot changeable
}
public boolean containsKey(final ORID iRID) {
return map.containsKey(iRID);
}
public ORecordInternal<?> remove(ORID iRID) {
return map.remove(iRID);
}
public void clear() {
map.clear();
}
public int size() {
return map.size();
}
public ORecordInternal<?> get(ORID identity) {
return map.get(identity);
}
public void put(ORID identity, ORecordInternal<?> iRecord) {
map.put(identity, iRecord);
}
public Iterable<ORecordInternal<?>> values() {
return map.values();
}
}