Безопасно ли хранить запись с карты? Может ли это вызвать утечку памяти?
Я бегу в этот код (адаптированный с фиктивными данными):
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
в кучу.
Тест 2: HashMap и хранение Map.Entry - PASS
Сама же программа, но вместо TreeMap
Я использую HashMap
,
gc
умеет собирать местные Map
экземпляры, несмотря на сохраненные Map.Entry
хранится в памяти, (Если вы пытались распечатать cache
результат после теста вы увидите реальные значения).
visualvm
графики:
Приложение не выдает никаких ошибок памяти.
Тест 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
периодически чистит карты.
Тест 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
) от сбора.
Надеюсь, это будет полезно для кого-то.