Безопасно ли хранить запись с карты? Может ли это вызвать утечку памяти?

Я бегу в этот код (адаптированный с фиктивными данными):

public Map<String, Integer> queryDatabase() {
    final Map<String, Integer> map = new TreeMap<>();
    map.put("one", 1);
    map.put("two", 2);
    // ...
    return map;
}

public Map.Entry<String, Integer> getEntry(int n) {
    final Map<String, Integer> map = queryDatabase();
    for (final Map.Entry<String, Integer> entry : map.entrySet()) {
        if (entry.getValue().equals(n)) return entry; // dummy check
    }
    return null;
}

Entry затем сохраняется во вновь созданном объекте, который сохраняется в кеше на неопределенный период:

class DataBundle {
    Map.Entry<String, Integer> entry;

    public void doAction() {
    this.entry = Application.getEntry(2);
    }
}

В то время как queryDatabase вызывается несколько раз в минуту, локальные Карты должны быть сброшены в последующем цикле gc. У меня есть основания полагать, что DataBundle сохраняя Entry ссылка предотвращает Map от сбора вообще.

Кроме того, java.util.TreeMap.Entry содержит несколько ссылок на братьев и сестер:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    // ...
}

Q: Хранит ли Map.Entry в поле члена сохранить местный Map экземпляры в память?

2 ответа

Решение

Контракт на Map.Entry не дает никаких обязательств в этой области, поэтому вы также не должны делать никаких предположений.

... Эти объекты Map.Entry действительны только на время итерации;...

По этой причине, если вы хотите хранить Key-Value пары, полученные из Map.Entry тогда вы должны взять копии.

Я написал приложение для бенчмаркинга, и результаты ясно показывают, что JVM не может собирать локальные данные. Map случаи, если Entry ссылка сохраняется в живых.

Это относится только к TreeMap Хотя и причина может быть в том, что TreeMap.Entry содержит различные ссылки на своих братьев и сестер.

Как упоминает @OldCurmudgeon,

вам не следует делать никаких предположений [и], если вы хотите хранить пары ключ-значение, полученные из Map.Entry, тогда вы должны взять копии

Я считаю, что на данный момент, если вы не знаете, что делаете, что угодно Map вы работаете с Map.Entry следует считать злом и анти-паттерном.

Всегда выбирайте сохранение копий Map.Entry или непосредственно сохранить ключ и значение.


Сравнительные технические данные:

JVM

java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)

ЦПУ

Caption           : Intel64 Family 6 Model 158 Stepping 9
DeviceID          : CPU0
Manufacturer      : GenuineIntel
MaxClockSpeed     : 4201
Name              : Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
SocketDesignation : LGA1151

баран

Model Name                  MaxCapacity MemoryDevices
----- ----                  ----------- -------------
      Physical Memory Array    67108864             4

Что будет делать тест?

  • Основная программа будет (псевдо) запрашивать базу данных 500 раз
  • Новый экземпляр DataBundle будет создаваться на каждой итерации и будет хранить случайный Map.Entry позвонив getEntry(int)
  • queryDatabase собирается создать местный TreeMap из 100_000 предметы на каждый звонок и getEntry просто вернет один Map.Entry с карты.
  • Все DataBundle экземпляры будут сохранены в ArrayList кэш.
  • Как DataBundle хранит Map.Entry будет отличаться между эталонами, чтобы продемонстрировать gc умения выполнять свой долг.
  • каждый 100 звонки в queryDatabase cache будет очищено: это, чтобы увидеть эффект от gc в visualvm

Тест 1: TreeMap и хранение Map.Entry - CRASH

DataBundle учебный класс:

class DataBundle {
    Map.Entry<String, Integer> entry = null;
    public DataBundle(int i) {
        this.entry = Benchmark_1.getEntry(i);
    }
}

Приложение для бенчмаркинга:

public class Benchmark_1 {
    static final List<DataBundle> CACHE = new ArrayList<>();
    static final int MAP_SIZE = 100_000;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 500; i++) {
            if (i % 100 == 0) {
                System.out.println("Clearing");
                CACHE.clear();
            }
            final DataBundle dataBundle = new DataBundle(new Random().nextInt(MAP_SIZE));
            CACHE.add(dataBundle);
            Thread.sleep(500); // to observe behavior in visualvm
        }
    }

    public static Map<String, Integer> queryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        for (int i = 0; i < MAP_SIZE; i++) map.put(String.valueOf(i), i);
        return map;
    }
    public static Map.Entry<String, Integer> getEntry(int n) {
        final Map<String, Integer> map = queryDatabase();
        for (final Map.Entry<String, Integer> entry : map.entrySet())
            if (entry.getValue().equals(n)) return entry;
        return null;
    }
}

Приложение не может даже достичь первого 100 итерации (очистка кэша), и он бросает java.lang.OutOfMemoryError:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.Integer.valueOf(Integer.java:832)
    at org.payloc.benchmark.Benchmark_1.queryDatabase(Benchmark_1.java:34)
    at org.payloc.benchmark.Benchmark_1.getEntry(Benchmark_1.java:38)
    at org.payloc.benchmark.DataBundle.<init>(Benchmark_1.java:11)
    at org.payloc.benchmark.Benchmark_1.main(Benchmark_1.java:26)
Mar 22, 2018 1:06:41 PM sun.rmi.transport.tcp.TCPTransport$AcceptLoop executeAcceptLoop
WARNING: RMI TCP Accept-0: accept loop for 
ServerSocket[addr=0.0.0.0/0.0.0.0,localport=31158] throws
java.lang.OutOfMemoryError: Java heap space
    at java.net.NetworkInterface.getAll(Native Method)
    at java.net.NetworkInterface.getNetworkInterfaces(NetworkInterface.java:343)
    at sun.management.jmxremote.LocalRMIServerSocketFactory$1.accept(LocalRMIServerSocketFactory.java:86)
    at sun.rmi.transport.tcp.TCPTransport$AcceptLoop.executeAcceptLoop(TCPTransport.java:400)
    at sun.rmi.transport.tcp.TCPTransport$AcceptLoop.run(TCPTransport.java:372)
    at java.lang.Thread.run(Thread.java:748)

*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create byte arrau at JPLISAgent.c line: 813

visualvm график ясно показывает, как сохраняется память, несмотря на gc выполняя некоторую активность в фоновом режиме, эрго: проведение одного Entry держит весь Map в кучу.

Benchmark_1: куча памяти

Benchmark_1: GC Activity

Тест 2: HashMap и хранение Map.Entry - PASS

Сама же программа, но вместо TreeMap Я использую HashMap,

gc умеет собирать местные Map экземпляры, несмотря на сохраненные Map.Entry хранится в памяти, (Если вы пытались распечатать cache результат после теста вы увидите реальные значения).

visualvm графики:

Benchmark_2: куча памяти Benchmark_2: GC Activity

Приложение не выдает никаких ошибок памяти.

Тест 3: TreeMap и хранение только ключа и значения (без Map.Entry) - PASS

Все еще использую TreeMap, но на этот раз вместо Map.Entry Я буду хранить ключ и данные значения напрямую.

class DataBundle3 {
    String key;
    Integer value;

    public DataBundle3(int i) {
        Map.Entry<String, Integer> e = Benchmark_3.getEntry(i);
        this.key = e.getKey();
        this.value = e.getValue();
    }
}

Безопасный подход, так как приложение правильно достигает конца и gc периодически чистит карты.

Benchmark_3: куча памяти Benchmark_3: GC Activity

Тест 4: TreeMap и кэш SoftReference (хранящий Map.Entry) - PASS

Не лучшее решение, но так как многие системы кэширования используют java.lang.ref.SoftReference Я опубликую тест с ним.

Итак, все еще используя TreeMap все еще храню Map.Entry в DataBundle, но используя список SoftReference<DataBundle>,

Итак, кеш становится:

static final List<SoftReference<DataBundle>> CACHE = new ArrayList<>();

и я сохраняю объекты через:

CACHE.add(new SoftReference<>(dataBundle));

Приложение завершается правильно, а gc бесплатно собирать карты в любое время. Это происходит потому, что SoftReference не сохраняет его referent (в нашем случае Map.Entry) от сбора.

Benchmark_4: куча памяти Benchmark_4: GC Activity


Надеюсь, это будет полезно для кого-то.

Другие вопросы по тегам