HashMap performance drops under heavy load

Scenario

Under peak traffic, CPU on map operations spikes. get/put that were microseconds become milliseconds. p99 latency grows while QPS is flat. Profilers show time inside HashMap or ConcurrentHashMap. You need to know whether the map is too big, badly hashed, resizing, or contended—and fix it without rewriting the whole service.

After reading, you should be able to:

Why — buckets stop behaving like O(1)

HashMap maps keys to an array of buckets using hashCode() and a spread function. Average case: one or few entries per bucket → fast. Worst case: all keys land in one bucket → linked list or tree walk per operation → effectively O(n). Under load, that looks like sudden “map degradation.”

Common degradation modes

ModeWhat happens
Hash collisionsBroken or hostile hashCode; all keys in one bucket
Resize / rehashTable doubles when load factor exceeded; all entries rehashed—CPU spike
Treeify pressure (Java 8+)Long chains become red-black trees—better than O(n) list but still hot
Huge unbounded mapMillions of entries; cache misses, GC scanning, long rehash
ConcurrentHashMap contentionMany threads hit same bin during resize or hot key updates
Wrong map typeHashMap shared across threads → external sync or corruption
Hot computeIfAbsentMany threads compute same missing key—duplicate expensive loads

HashMap vs ConcurrentHashMap under load

Collisions ≠ races. Slow maps can be correct but poorly distributed. Concurrent modification of plain HashMap causes exceptions or bugs—see shared data races.

What — prove the bottleneck is the map

  1. Profiler (CPU or wall-clock) — async-profiler, JFR, YourKit: hot frames in HashMap.getNode, putVal, ConcurrentHashMap.transfer, treeifyBin.
  2. Correlate with traffic and map size — metrics: cache/map entry count, heap used by map (approximate via heap dump dominator tree). Spike at round entry counts (64k, 128k) → resize event.
  3. Inspect key type — custom hashCode() returning constant? Only equals without consistent hashCode? Mutable keys changed after insert?
  4. Check for collision patterns — security: Hash DoS with crafted String keys (mitigated in modern JDKs with per-map seed, but custom keys still vulnerable). — domain: IDs that collide mod bucket count if hash is weak.
  5. Concurrent access pattern — one hot key updated by all threads? Metrics per key if possible (business id in logs).
  6. Anti-patterns in hot loops
    // CHM size() can be expensive — walks segments
    for (...) {
      if (cache.size() > MAX) evict();  // avoid
    }
  7. Thread dump (secondary) — many threads in map methods or blocked on CHM bin—pairs with BLOCKED / short freezes during resize.

Smoking-gun examples

// Bad: constant hash — all keys in one bucket
@Override public int hashCode() { return 1; }

// Bad: default capacity for millions of inserts — many resizes
Map<String, Data> cache = new HashMap<>();

// Bad: hot global key
rateLimitMap.computeIfAbsent("GLOBAL", k -> new Counter());

How — restore map performance

Fixes (in priority order)

FixWhen
Fix hashCode / use immutable keysCustom keys; use Objects.hash or record types
Pre-size the mapKnown N inserts: new HashMap<>((int)(n / 0.75f) + 1)
Bound sizeCaffeine/Guava cache with maximumSize + TTL—not unbounded ConcurrentHashMap
Shard mapsmap[hash(id) % 16] reduces contention per CHM instance
Split hot keysPer-tenant counters instead of one global key
Right structureCHM for concurrent cache; HashMap only per-request
Load once per keycomputeIfAbsent + single-flight wrapper for expensive loads

Pre-sized ConcurrentHashMap

int expected = 100_000;
Map<String, Session> sessions = new ConcurrentHashMap<>(expected);

JDK picks internal size from expected entries; avoids repeated resize while warming cache.

Bounded cache (Caffeine sketch)

Cache<String, User> users = Caffeine.newBuilder()
    .maximumSize(50_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .build();

When to leave HashMap entirely

Verify

  1. Load test at peak: map methods no longer top CPU; p99 back within SLO.
  2. Entry count stable or bounded; no stair-step latency at resize boundaries.
  3. Heap: map dominator size flat over 24h soak.

Interview one-liner

“HashMap degrades when keys collide into one bucket, the table keeps resizing, or ConcurrentHashMap contends on hot keys. I profile get/put, fix hashCode and initial capacity, bound the cache, and shard or use a proper cache library if it’s shared under high concurrency.”

Related scenarios