🚀 I Put 1.5 Million Entries into a Java HashMap. Here's What I Learned Using VisualVM.
“We all know
HashMapprovides average O(1) operations. But what actually happens inside the JVM when you insert one million or more entries?"
I’ve used HashMap almost every day for years, but I had never actually measured how the JVM stores millions of entries.
Instead of reading another article explaining buckets and hashing, I wanted to answer questions like:
- How much heap does it really consume?
- What objects are created?
- What is
HashMap$Node? - Why does VisualVM show
HashMap$Node[]? - Does pre-sizing really help?
- How gracefully does the JVM handle millions of entries?
So I decided to run a small experiment.
Environment
- Java 17 (Temurin)
- Spring Tool Suite
- VisualVM 2.2.1
- macOS Intel
The benchmark is intentionally simple.
Map<Integer, Integer> map = new HashMap<>();for (int i = 0; i < entries; i++) {
map.put(i, i);
}Thread.sleep(300000);
The sleep allows attaching VisualVM after insertion.
Experiment 1–100K Entries
Console
Insertion Finished
Time : 43 msHeap Dump
📷 Screenshot 1
The heap dump shows approximately
ObjectCountInteger 200,033
HashMap$Node 104,401
HashMap$Node[] 1,033
My first question was…
Why are there 200K Integer objects for only 100K entries?
The answer is simple.
Every insertion
map.put(i, i);creates
Integer Key+Integer Value+HashMap Node
So
100K entries↓100K Keys+100K Values↓≈200K Integer Objects
Exactly what VisualVM reported.
Understanding HashMap$Node
Every entry creates one internal node.
Conceptually,
Nodehashkeyvaluenext
VisualVM showed approximately one HashMap$Node per map entry.
The Most Confusing Part — HashMap$Node[]
Initially I thought
Isn’t the bucket array just storing nodes?
Not exactly.
The bucket array stores references.
HashMap↓Bucket Array↓Reference↓Node
Think of it like
Bucket[0] ----> NodeBucket[1] ----> nullBucket[2] ----> Node
The array and the node objects are completely different heap objects.
That explains why VisualVM reports them separately.
Experiment 2–1 Million Entries
Console
Max Heap : 2048 MBHeap Used : 85 MBInsertion Time : 318 ms
Heap Dump
📷 Screenshot 2
VisualVM now reported approximately
Get Rvjh’s stories in your inbox
Join Medium for free to get updates from this writer.
ObjectCountHashMap$Node 1,004,401
Integer 2,000,033
Bucket Arrays Same number of arrays, but much larger
One interesting observation:
The JVM handled one million entries surprisingly well.
No OutOfMemoryError.
No unusual slowdown.
Just predictable heap growth.
Experiment 3 — Does Pre-sizing Help?
Now I compared
new HashMap<>();with
new HashMap<>(2_097_152);(the capacity is large enough to avoid most resizes for ~1.5M entries).Without Pre-sizing
1.5 Million EntriesHeap Used : 115 MBInsertion Time : 552 ms
With Pre-sizing
1.5 Million EntriesHeap Used : 105 MBInsertion Time : 335 ms
Heap Dump
📷 Screenshot 3
Results
ConfigurationHeap UsedInsert TimeDefault
HashMap 115 MB 552 ms
Pre-sized HashMap 105 MB 335 ms
The interesting part wasn’t memory.
It was time.
Pre-sizing reduced insertion time by almost 40%.
Why?
Because the default HashMap resized multiple times.
Each resize involves:
- allocating a larger bucket array
- recomputing bucket indexes
- redistributing existing nodes
Pre-sizing avoids most of that work.
The final memory usage is almost the same because both versions ultimately store the same number of objects.
even after increasing from 100K to 1 million entries.
One thing initially confused me.
VisualVM always showed around
HashMap$Node[]1033 objectseven after increasing from 100K to 1 million entries.
Why?
Because VisualVM is showing all live HashMaps in the JVM, not just mine.
The important thing wasn’t the number of arrays.
It was their size.
For example, my benchmark HashMap eventually had a bucket array containing over 2 million references, which VisualVM exposed when expanding the object tree.
That was my “aha!” moment.
What I Learned
This small experiment taught me much more than reading documentation.
✅ Every map entry creates one HashMap$Node.
✅ Keys and values are separate objects.
✅ Bucket arrays store references, not nodes.
✅ The JVM grows heap gradually instead of immediately allocating the maximum heap.
✅ Pre-sizing doesn’t significantly reduce final memory usage.
✅ Pre-sizing noticeably improves insertion performance by avoiding repeated resizing and rehashing.
Why I Think This Matters
Most articles explain how HashMap works.
Very few actually measure it.
Running a small benchmark and opening a heap dump in VisualVM completely changed my understanding.
Instead of assuming how the JVM behaves, I could actually see:
- object counts
- heap usage
- bucket arrays
- internal nodes
That made the implementation much easier to understand.
What’s Next?
I’m now curious to run similar experiments with:
ConcurrentHashMapArrayListPriorityQueueLinkedHashMap- Large custom objects
- Different GC algorithms
- 10 million+ entries
Final Thoughts
One of the biggest lessons from this exercise wasn’t about HashMap itself—it was about measuring instead of assuming.
As backend engineers, we often discuss performance in terms of Big-O notation. That’s important, but pairing that knowledge with tools like VisualVM gives a much clearer picture of how our code behaves in the JVM.
If you’re preparing for Java interviews or simply curious about JVM internals, I’d encourage you to try a similar experiment. It’s simple to set up, and you’ll come away with a much deeper understanding than theory alone can provide.
Note:
“This experiment isn’t meant to benchmark HashMap exhaustively. The goal was to build intuition by observing how the JVM represents and manages a large in-memory data structure using VisualVM. If it sparks curiosity for someone else to run similar experiments, it has served its purpose."





