Использование MapMaker для создания кэша

Я хочу использовать 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();

Записи никогда не удаляются сборщиком мусора, и я получаю сообщение об ошибке нехватки памяти в своей тестовой программе. Метод finalize для записей 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? Можно ли добавить эталонную очередь на карту кеша?


person Michael    schedule 09.07.2010    source источник
comment
Может ли кто-нибудь отредактировать код, чтобы его было легче читать?   -  person nanda    schedule 09.07.2010
comment
Я немного удивлен этим. Я использовал softValues точно так же, и он работал нормально, а SoftReference очищались, когда не хватало памяти.   -  person finnw    schedule 17.07.2010


Ответы (3)


Есть много вещей, которые могут происходить, но в отношении вашей тестовой программы, использующей мягкие значения: вы можете получить OutOfMemoryError, даже если у вас есть SoftReferences, которые еще не были удалены сборщиком мусора. Это стоит повторить: вы можете получить OutOfMemoryError, даже если у вас есть SoftReferences, которые еще не очищены.

SoftReferences немного странные, см. http://jeremymanson.blogspot.com/2009/07/how-hotspot-decides-to-clear_07.html для описания текущей механики. Скорее всего, в вашем тестовом случае сборщик мусора просто не успел провести два полных сборщика мусора.

Когда вы использовали weakKeys, компьютерная графика сразу очищала их, и вам не нужно было ждать полной паузы сборщика мусора. (b/c WeakReferences собираются агрессивно.)

На мой взгляд, если вам нужен чувствительный к памяти кеш с целочисленными ключами, я думаю, что уместно следующее:

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;

Удачи.

person Darren Gilroy    schedule 10.07.2010
comment
+1, потому что я не понимал, что вы можете получить OutOfMemoryError до того, как все SoftReferences будут проверены. - person Jon Quarfoth; 06.08.2010

Слабые ключи кажутся ошибкой. Попробуйте использовать надежные ключи, так как они являются целыми числами.

person Craig P. Motlin    schedule 09.07.2010
comment
Я попробовал это, и это сработает, если я вызову System.gc() до того, как создам новый объект и добавлю его в кеш. Если я этого не сделаю, рано или поздно я получу исключение нехватки памяти. Это правильный подход или вы рекомендуете что-то другое? - person Michael; 09.07.2010
comment
У вас есть две версии TestValue, одна содержит большой массив, а другая просто содержит целое число. Вы тестируете с большим массивом? Если нет, возможно, GC просто не может освободить достаточно памяти. - person Craig P. Motlin; 09.07.2010

Я хотел бы обратить ваше внимание на Suppliers.memoizeWithExpirationm, Instant-cache.

http://guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/common/base/Suppliers.html#memoizeWithExpiration(com.google.common.base.Supplier, длинный, java.util.concurrent.TimeUnit)

person BjornS    schedule 09.08.2010
comment
Прохладный! Но нет упоминания о RefereceTypes == нет гарантии, что ссылка будет освобождена до того, как из-за ошибки памяти - person bjornl; 28.02.2011