Chapter 15: Data Structure Selection and Implementation Trade-offs
15.02Beginner / Intermediate6 topics15 min1 interactive animation

Memory models

What data structures cost in memory on a real JVM: contiguous arrays, node objects, hash buckets, tree pointers, heap fragmentation and garbage-collection pressure, with sizes measured on HotSpot.

Learning goals

  • Estimate the bytes per element of arrays, linked lists, hash maps and tree maps
  • Explain why contiguous storage is faster than pointer chasing
  • Describe how boxing and node objects create garbage-collection work

Prerequisites

Operation matricesComplexity, memory and performance analysis
15.02.1

Array contiguous storage

An int[] stores its values back to back after a small header, so int[n] costs 16 + 4n bytes rounded up to a multiple of 8 (an int[10] is 56 bytes and an int[100] is 416 on a 64-bit HotSpot JVM with compressed references). Elements that are neighbours in the index are neighbours in memory, so the CPU's cache loads several per cache line and its prefetcher predicts the next ones.

This is why scanning an array is fast even though it is “just O(n)” like a list scan. The animation compares reading a matrix row by row (memory order) with column by column (jumping across memory).

ObjectMeasured size (bytes)
int[0]16
int[10]56
int[100]416
Object[10] (references)56
Integer16

Cache lines: row-major vs column-major traversal

Interactive · edit the input

Your input

Try

memory (row-major, 4 ints per cache line)

0line 0
1line 0
2line 0
3line 0
4line 1
5line 1
6line 1
7line 1
8line 2
9line 2
10line 2
11line 2
12line 3
13line 3
14line 3
15line 3
16line 4
17line 4
18line 4
19line 4
20line 5
21line 5
22line 5
23line 5
24line 6
25line 6
26line 6
27line 6
28line 7
29line 7
30line 7
31line 7

cache (2 lines, LRU)

empty
Setup

A 4×8 matrix is stored row by row in one long run of memory. The CPU fetches memory in cache lines of 4 ints and keeps the 2 most recently used lines.

Step 1 / 34
Java
1int sumRowMajor(int[][] m) { // inner loop walks along a row
2 int sum = 0;
3 for (int r = 0; r < m.length; r++)
4 for (int c = 0; c < m[0].length; c++) sum += m[r][c];
5 return sum;
6}
7int sumColumnMajor(int[][] m) { // inner loop jumps down a column
8 int sum = 0;
9 for (int c = 0; c < m[0].length; c++)
10 for (int r = 0; r < m.length; r++) sum += m[r][c];
11 return sum;
12}

Live state

0 hits0 misses

rows

4

cols

8

lineSize

4

cache lines

2

No condition evaluated on this step
Execution trace (dry run table)Show 34 steps
StepLineStateWhat happens
1L3rows=4 cols=8 lineSize=4 cache lines=2A 4×8 matrix is stored row by row in one long run of memory. The CPU fetches memory in cache lines of 4 ints and keeps the 2 most recently used lines.
2L4r=0 c=0 address=0 line=0 hits=0 misses=1m[0][0] is memory cell 0, on line 0, which is not cached: a miss. The CPU stalls while the whole line is loaded.
3L4r=0 c=1 address=1 line=0 hits=1 misses=1m[0][1] is memory cell 1, on line 0, which is already in the cache: a hit (fast).
4L4r=0 c=2 address=2 line=0 hits=2 misses=1m[0][2] is memory cell 2, on line 0, which is already in the cache: a hit (fast).
5L4r=0 c=3 address=3 line=0 hits=3 misses=1m[0][3] is memory cell 3, on line 0, which is already in the cache: a hit (fast).
6L4r=0 c=4 address=4 line=1 hits=3 misses=2m[0][4] is memory cell 4, on line 1, which is not cached: a miss. The CPU stalls while the whole line is loaded.
7L4r=0 c=5 address=5 line=1 hits=4 misses=2m[0][5] is memory cell 5, on line 1, which is already in the cache: a hit (fast).
8L4r=0 c=6 address=6 line=1 hits=5 misses=2m[0][6] is memory cell 6, on line 1, which is already in the cache: a hit (fast).
9L4r=0 c=7 address=7 line=1 hits=6 misses=2m[0][7] is memory cell 7, on line 1, which is already in the cache: a hit (fast).
10L4r=1 c=0 address=8 line=2 hits=6 misses=3m[1][0] is memory cell 8, on line 2, which is not cached: a miss. The CPU stalls while the whole line is loaded.
11L4r=1 c=1 address=9 line=2 hits=7 misses=3m[1][1] is memory cell 9, on line 2, which is already in the cache: a hit (fast).
12L4r=1 c=2 address=10 line=2 hits=8 misses=3m[1][2] is memory cell 10, on line 2, which is already in the cache: a hit (fast).
13L4r=1 c=3 address=11 line=2 hits=9 misses=3m[1][3] is memory cell 11, on line 2, which is already in the cache: a hit (fast).
14L4r=1 c=4 address=12 line=3 hits=9 misses=4m[1][4] is memory cell 12, on line 3, which is not cached: a miss. The CPU stalls while the whole line is loaded.
15L4r=1 c=5 address=13 line=3 hits=10 misses=4m[1][5] is memory cell 13, on line 3, which is already in the cache: a hit (fast).
16L4r=1 c=6 address=14 line=3 hits=11 misses=4m[1][6] is memory cell 14, on line 3, which is already in the cache: a hit (fast).
17L4r=1 c=7 address=15 line=3 hits=12 misses=4m[1][7] is memory cell 15, on line 3, which is already in the cache: a hit (fast).
18L4r=2 c=0 address=16 line=4 hits=12 misses=5m[2][0] is memory cell 16, on line 4, which is not cached: a miss. The CPU stalls while the whole line is loaded.
19L4r=2 c=1 address=17 line=4 hits=13 misses=5m[2][1] is memory cell 17, on line 4, which is already in the cache: a hit (fast).
20L4r=2 c=2 address=18 line=4 hits=14 misses=5m[2][2] is memory cell 18, on line 4, which is already in the cache: a hit (fast).
21L4r=2 c=3 address=19 line=4 hits=15 misses=5m[2][3] is memory cell 19, on line 4, which is already in the cache: a hit (fast).
22L4r=2 c=4 address=20 line=5 hits=15 misses=6m[2][4] is memory cell 20, on line 5, which is not cached: a miss. The CPU stalls while the whole line is loaded.
23L4r=2 c=5 address=21 line=5 hits=16 misses=6m[2][5] is memory cell 21, on line 5, which is already in the cache: a hit (fast).
24L4r=2 c=6 address=22 line=5 hits=17 misses=6m[2][6] is memory cell 22, on line 5, which is already in the cache: a hit (fast).
25L4r=2 c=7 address=23 line=5 hits=18 misses=6m[2][7] is memory cell 23, on line 5, which is already in the cache: a hit (fast).
26L4r=3 c=0 address=24 line=6 hits=18 misses=7m[3][0] is memory cell 24, on line 6, which is not cached: a miss. The CPU stalls while the whole line is loaded.
27L4r=3 c=1 address=25 line=6 hits=19 misses=7m[3][1] is memory cell 25, on line 6, which is already in the cache: a hit (fast).
28L4r=3 c=2 address=26 line=6 hits=20 misses=7m[3][2] is memory cell 26, on line 6, which is already in the cache: a hit (fast).
29L4r=3 c=3 address=27 line=6 hits=21 misses=7m[3][3] is memory cell 27, on line 6, which is already in the cache: a hit (fast).
30L4r=3 c=4 address=28 line=7 hits=21 misses=8m[3][4] is memory cell 28, on line 7, which is not cached: a miss. The CPU stalls while the whole line is loaded.
31L4r=3 c=5 address=29 line=7 hits=22 misses=8m[3][5] is memory cell 29, on line 7, which is already in the cache: a hit (fast).
32L4r=3 c=6 address=30 line=7 hits=23 misses=8m[3][6] is memory cell 30, on line 7, which is already in the cache: a hit (fast).
33L4r=3 c=7 address=31 line=7 hits=24 misses=8m[3][7] is memory cell 31, on line 7, which is already in the cache: a hit (fast).
34L5accesses=32 hits=24 misses=8 hit rate %=75 return=88 misss and 24 hits (75% hit rate). Both loops do identical arithmetic and are O(rows × cols), but walking along a row uses every int in a loaded line before moving on. Switch the order and compare.
15.02.2

Node object overhead

Every object carries a header (12 bytes with compressed references) and is padded to a multiple of 8 bytes. A LinkedList node holds an item reference and two links, which makes it 24 bytes, and the Integer it points to is another 16 bytes. One element therefore costs 40 bytes plus the 4-byte reference that points at the node, against 4 bytes in an int[]: about ten times more.

Worse, nodes are allocated at different times and may sit far apart, so traversing a linked list means chasing pointers, and each hop can be a cache miss. A singly linked node you write yourself, with a primitive int field and one link, measures 24 bytes as well: it saves the 16-byte Integer but is still scattered in memory.

Per elementBytes
int[] element4
Integer[] element: reference + Integer object4 + 16 = 20
LinkedList<Integer> element: node + Integer24 + 16 = 40
15.02.3

Hash bucket overhead

A HashMap entry is a node with the cached hash, the key and value references and the next link: 32 bytes. Add 16 bytes for each boxed Integer key and value, and 4 bytes for its slot in the table (which is kept at most 75% full, so about 8 bytes per entry on average). Measured on a map of one million Integer to Integer entries (keys above the small-value cache) the total was 72,388,672 bytes in a table of 2,097,152 slots: roughly 72 bytes per entry.

The same data in two int[] arrays would be 8 MB. When keys are small and dense, an array indexed by the key replaces the map completely.

Part of one HashMap<Integer,Integer> entryBytes
HashMap.Node32
Integer key (values above 127)16
Integer value (values above 127)16
Table slot, amortised at load factor 0.5-0.75about 6-8
15.02.4

Tree pointers

A TreeMap entry stores key, value, left, right and parent references plus a colour flag: 40 bytes per entry on the measured JVM, plus the boxed key and value. Each lookup follows O(log n) pointers, each of them a potential cache miss, which is why a TreeMap is often several times slower than a HashMap for pure lookups even though both are “fast”.

What a tree buys is order: sorted iteration, floor and ceiling, ranges. If you do not need order, the hash table is smaller and faster; if you do, an array that is sorted once and searched with binary search is smaller still, as long as it rarely changes.

Structure (per entry, excluding boxed key and value)Bytes
int in a sorted int[]4
HashMap.Node32
TreeMap.Entry40
LinkedList.Node24
15.02.5

Heap fragmentation

Java's collectors move live objects and compact free space, so the classic C-style fragmentation, where free memory exists but not in one piece, is mostly not a problem for small objects. The practical effect is *locality*: objects allocated together tend to be close, but a structure built up over time (a linked list, a tree, a map) ends up scattered, and each pointer hop may touch a new cache line.

Large arrays are the exception: a collector may need one contiguous region for a very large array and can fail or work harder when none is free. The remedies are to reuse big arrays, to allocate them early and to avoid many short-lived big buffers. A compact array of primitives is the opposite of a fragmented structure.

15.02.6

GC pressure

The garbage collector's work grows with the number of objects it must trace and with how fast you create garbage. A List<Integer> of a million values is a million-and-one objects, 20 MB measured (20,000,016 bytes for an Integer[] and its Integer objects), where an int[] of the same values is one object of 4 MB (4,000,016 bytes). Every boxed value, every node and every temporary Entry is something to allocate, trace and eventually reclaim.

Practical consequences: prefer primitive arrays in tight loops, avoid boxing in hot code (int instead of Integer keys where possible), reuse buffers and presize collections so they do not resize and discard their old arrays, and keep short-lived objects short-lived, because young-generation collection is cheap for objects that die quickly.

One million valuesObjectsMeasured bytes
int[1_000_000]14,000,016
Integer[1_000_000] plus its Integer objects (values above 127)1,000,00120,000,016
HashMap<Integer,Integer>, 1,000,000 entriesabout 3,000,00072,388,672

Practice ladder

Now try it yourself

Easy

  • How many bytes does an int[1000] occupy?
  • Why is an array scan faster than a linked-list scan with the same O(n)?

Medium

  • Estimate the memory of a HashMap<Integer,Integer> with 100,000 entries
  • When is a sorted array a better map than TreeMap?

Advanced

  • Design an int-to-int hash map with open addressing and compare its memory with HashMap

Key takeaways

  • Arrays are compact and cache-friendly; linked nodes, hash entries and tree entries cost 24 to 40 bytes plus boxed values.
  • Pointer-heavy structures are scattered in memory, so traversals suffer cache misses.
  • Fewer objects means less GC work: prefer primitives and presized arrays in hot paths.