Я хочу использовать MapMaker для создания карты, что большие объекты кэшей, которые должны быть удалены из кэша, если существует недостаточно памяти. Эта небольшая демонстрационная программа, кажется, хорошо работает:
public class TestValue {
private final int id;
private final int[] data = new int[100000];
public TestValue(int id) {
this.id = id;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalized");
}
}
public class Main {
private ConcurrentMap<Integer, TestValue> cache;
MemoryMXBean memoryBean;
public Main() {
cache = new MapMaker()
.weakKeys()
.softValues()
.makeMap();
memoryBean = ManagementFactory.getMemoryMXBean();
}
public void test() {
int i = 0;
while (true) {
System.out.println("Etntries: " + cache.size() + " heap: "
+ memoryBean.getHeapMemoryUsage() + " non-heap: "
+ memoryBean.getNonHeapMemoryUsage());
for (int j = 0; j < 10; j++) {
i++;
TestValue t = new TestValue(i);
cache.put(i, t);
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}
}
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Main m = new Main();
m.test();
}
}
Однако, когда я делаю то же самое в своем реальном приложении, записи в основном удалены из кэша, как только они добавляются. В моем реальном приложении я также использую целые числа в качестве ключей, и кэшируемые значения являются блоками архива, считанными из диска, который содержит некоторые данные. Насколько я понимаю, слабые ссылки собраны "мусор", как только они больше не используются, таким образом, это, кажется, имеет смысл, потому что ключи являются слабыми ссылками. Если я создаю карту как это:
data = new MapMaker()
.softValues()
.makeMap();
Записи никогда не собираются "мусор", и я получаю ошибку из памяти в своей тестовой программе. Завершить метод на записях TestValue никогда не называют. Если я изменяю метод тестирования для следующего:
public void test() {
int i = 0;
while (true) {
for (final Entry<Integer, TestValue> entry :
data.entrySet()) {
if (entry.getValue() == null) {
data.remove(entry.getKey());
}
}
System.out.println("Etntries: " + data.size() + " heap: "
+ memoryBean.getHeapMemoryUsage() + " non-heap: "
+ memoryBean.getNonHeapMemoryUsage());
for (int j = 0; j < 10; j++) {
i++;
TestValue t = new TestValue(i);
data.put(i, t);
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}
}
}
записи удалены из кэша, и финализатор на объектах TestValue называют, но через некоторое время я также получаю ошибку из памяти.
Таким образом, мой вопрос: что правильный путь состоит в том, чтобы использовать MapMaker для создания карты, которая может использоваться в качестве кэша? Почему моя тестовая программа не удаляет записи как можно скорее, если я использую weakKeys? Действительно ли возможно добавить ссылочную очередь к карте кэша?
Слабые клавиши кажутся ошибкой. Попробуйте использовать надежные ключи, поскольку они целые.
Есть много вещей, которые могут происходить, но что касается вашей тестовой программы, использующей мягкие значения: вы можете получить OutOfMemoryError, даже если у вас есть SoftReferences, которые еще не были собраны в мусор. Стоит повторить: вы можете получить OutOfMemoryError, даже если у вас есть SoftReferences, которые еще не были очищены.
SoftReferences немного странная штука, см. http://jeremymanson.blogspot.com/2009/07/how-hotspot-decides-to-clear_07.html для описания текущей механики. Вероятно, в вашем тестовом случае у GC просто не было времени на два полных GC.
Когда вы использовали weakKeys, CG очистил их сразу же, и ему не пришлось ждать полной паузы GC. (b/c WeakReferences собираются агрессивно.)
На мой взгляд, если вам нужен чувствительный к памяти кэш с Integer ключами, я бы подумал, что подходит следующее:
data = new MapMaker().softValues().makeMap();
Вы можете легко сделать тестовую программу, которая выбрасывает OutOfMemoryError, но если ваше реальное приложение ведет себя хорошо и не испытывает слишком больших нагрузок, все может быть в порядке. SoftReferences довольно трудно сделать правильно.
Если вам нужно использовать System.gc(), чтобы избежать выхода из памяти, я бы рекомендовал перейти на карту LRU с фиксированным максимальным размером (см. javadoc java.util.LinkedHashMap для примера). Это не параллельно, но я ожидаю, что это даст вам лучшую пропускную способность в конце концов, чем просить систему выполнить сборку мусора с полной паузой кучу дополнительных раз.
О, и последнее замечание о целочисленных ключах и weakKeys(): MapMaker использует сравнение идентичности для ключей при использовании слабых или мягких ключей, и это довольно сложно сделать правильно. Свидетельство тому следующее:
Map<Integer,String> map = new MapMaker().weakKeys().makeMap();
Integer a = new Integer(1);
Integer b = new Integer(1);
Integer c = 1; //auto box
Integer d = 1; //auto box
map.put(a, "A");
map.put(b, "B");
map.put(c,"C");
map.put(d,"D");
map.size() // size is 3;
Удачи.
Я хотел бы обратить ваше внимание на Suppliers.memoizeWithExpirationm, мгновенный кеш.
http://guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/common/base/Suppliers.html#memoizeWithExpiration (com.google.common.base.Supplier , длинный, java.util.concurrent.TimeUnit)