Skip to content

Commit f1b2929

Browse files
committed
fix: avoid OutOfMemoryError when deserializing CompactHashMap
Previously, the keys were serialized in a semi-random order which might trigger OOM when deserializing the map. Now we always serialize keys in the creation order, so the same order is reproduced when deserializing maps. Fixes #9
1 parent f641491 commit f1b2929

File tree

3 files changed

+53
-34
lines changed

3 files changed

+53
-34
lines changed

compactmap/src/main/java/vlsi/utils/CompactHashMapClass.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ abstract class CompactHashMapClass<K, V> {
2727
public static final CompactHashMapClass EMPTY = new CompactHashMapClassEmptyDefaults(
2828
new com.github.andrewoma.dexx.collection.HashMap());
2929

30+
/**
31+
* Enum to represent "value was removed" in the serialized representation of the map.
32+
*/
33+
enum RemovedObjectMarker {
34+
INSTANCE;
35+
}
36+
3037
final com.github.andrewoma.dexx.collection.Map<K, Integer> key2slot; // Immutable
3138

3239
// This value is used as a marker of deleted object
@@ -70,14 +77,16 @@ private Object getInternal(CompactHashMap<K, V> map, Object key) {
7077

7178
protected static Object getValueFromSlot(CompactHashMap map, int slot) {
7279
switch (slot) {
73-
case -1:
74-
return map.v1;
7580
case -2:
76-
return map.v2;
77-
case -3:
7881
return map.v3;
82+
case -1:
83+
return map.v2;
84+
case 0:
85+
// Maps with fewer than 3 keys store their slot 0 in v1
86+
if (map.klass.key2slot.size() <= 3) {
87+
return map.v1;
88+
}
7989
}
80-
8190
return ((Object[]) map.v1)[slot];
8291
}
8392

@@ -103,21 +112,23 @@ public V put(CompactHashMap<K, V> map, K key, Object value) {
103112

104113
switch (slot) {
105114
case -1:
106-
if (prevValue == REMOVED_OBJECT)
107-
prevValue = map.v1;
108-
map.v1 = value;
109-
break;
110-
case -2:
111115
if (prevValue == REMOVED_OBJECT)
112116
prevValue = map.v2;
113117
map.v2 = value;
114118
break;
115-
case -3:
119+
case -2:
116120
if (prevValue == REMOVED_OBJECT)
117121
prevValue = map.v3;
118122
map.v3 = value;
119123
break;
120124
default:
125+
if (slot == 0 && key2slot.size() <= 3) {
126+
if (prevValue == REMOVED_OBJECT)
127+
prevValue = map.v1;
128+
map.v1 = value;
129+
break;
130+
}
131+
121132
Object[] array = (Object[]) map.v1;
122133
if (prevValue == REMOVED_OBJECT)
123134
prevValue = array[slot];
@@ -206,16 +217,26 @@ public Set<Map.Entry<K, V>> entrySet(CompactHashMap<K, V> map) {
206217
public void serialize(final CompactHashMap<K, V> map, final ObjectOutputStream s) throws IOException {
207218
// We serialize default and non default values separately
208219
// That makes serialized representation more compact when several maps share defaults
209-
int size = key2slot.size() - removedSlotsCount(map);
210-
s.writeInt(size);
211-
212-
if (size > 0)
213-
for (Pair<K, Integer> entry : key2slot) {
214-
Object value = getValueFromSlot(map, entry.component2());
215-
if (value == REMOVED_OBJECT) continue;
216-
s.writeObject(unmaskNull(entry.component1()));
217-
s.writeObject(value);
220+
int ownKeys = key2slot.size();
221+
s.writeInt(ownKeys);
222+
// Write keys in the order they were added to the map, so deserialization reuses key2slot instances
223+
if (ownKeys > 0) {
224+
// Slots are always -2..(map.size-2), so we do not need sort
225+
final Object[] keys = new Object[ownKeys];
226+
key2slot.forEach(new com.github.andrewoma.dexx.collection.Function<Pair<K, Integer>, Void>() {
227+
public Void invoke(Pair<K, Integer> entry) {
228+
keys[entry.component2() + 2] = entry.component1();
229+
return null;
230+
}
231+
});
232+
for (int i = 0; i < keys.length; i++) {
233+
Object key = keys[i];
234+
Object value = getValueFromSlot(map, i - 2);
235+
s.writeObject(unmaskNull((K) key));
236+
// We write REMOVED_OBJECT as well to keep the same key2slot when deserializing
237+
s.writeObject(value == REMOVED_OBJECT ? RemovedObjectMarker.INSTANCE : value);
218238
}
239+
}
219240

220241
// Serialize default values as separate map
221242
s.writeObject(getDefaultValues());
@@ -228,7 +249,14 @@ public static <K, V> void deserialize(CompactHashMap<K, V> map, ObjectInputStrea
228249
for (int i = 0; i < size; i++) {
229250
K key = (K) s.readObject();
230251
V value = (V) s.readObject();
231-
map.put(key, value);
252+
if (value == RemovedObjectMarker.INSTANCE) {
253+
// We cannot use put(key, REMOVED_OBJECT) as the map would just ignore it
254+
// We need to add an entry first so it allocates a new slot
255+
map.put(key, null);
256+
map.remove(key);
257+
} else {
258+
map.put(key, value);
259+
}
232260
}
233261

234262
Map<K, V> defaults = (Map<K, V>) s.readObject();

compactmap/src/main/java/vlsi/utils/CompactHashMapClassEmptyDefaults.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535

3636
package vlsi.utils;
3737

38-
import com.github.andrewoma.dexx.collection.Pair;
39-
4038
import java.util.Collections;
4139
import java.util.HashMap;
4240
import java.util.IdentityHashMap;
@@ -99,16 +97,7 @@ protected CompactHashMapClass<K, V> getNextKlass(K key, Map<K, V> defaultValues)
9997
int size = key2slot.size();
10098
com.github.andrewoma.dexx.collection.Map<K, Integer> newKey2slot = key2slot;
10199

102-
if (size < 3) size -= 3;
103-
else if (size == 3) {
104-
size = 1;
105-
for (Pair<K, Integer> entry : key2slot)
106-
if (entry.component2() == -1) {
107-
newKey2slot = newKey2slot.put(entry.component1(), 0);
108-
break;
109-
}
110-
} else size -= 2;
111-
newKey2slot = newKey2slot.put(key, size);
100+
newKey2slot = newKey2slot.put(key, size - 2);
112101

113102
newKlass = new CompactHashMapClassEmptyDefaults<K, V>(newKey2slot);
114103
synchronized (this) {

compactmap/src/main/java/vlsi/utils/CompactHashMapDefaultValues.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.HashMap;
2020
import java.util.IdentityHashMap;
21+
import java.util.LinkedHashMap;
2122
import java.util.Map;
2223
import java.util.concurrent.locks.Lock;
2324
import java.util.concurrent.locks.ReadWriteLock;
@@ -85,7 +86,8 @@ public static <K, V> Map<K, V> getNewDefaultValues(Map<K, V> prevDefaultValues,
8586
readLock.unlock();
8687
}
8788

88-
Map<K, V> newMap = new HashMap<K, V>((int) ((prevDefaultValues.size() + 1) / 0.75f));
89+
// Keep the order of entries in the default values, so we have a consistent subset of "default class"
90+
Map<K, V> newMap = new LinkedHashMap<K, V>((int) ((prevDefaultValues.size() + 1) / 0.75f));
8991

9092
newMap.putAll(prevDefaultValues);
9193

0 commit comments

Comments
 (0)