skip to content

Given a `jmap -histo` output sample, how do you read it and decide which class to investigate first?

level: middleimportance: nice to knowfreq 30%

answer

  1. Columns: rank, #instances, #bytes, class name
  2. Sorted by bytes desc; top = low-level types
  3. [B=byte[], [C=char[], [I=int[]
  4. Look past arrays/String for YOUR classes
  5. Diff two histograms → monotonic growth = leak

basics

~20 s

Each row shows a class with how many instances exist and how many bytes they use, sorted with the biggest memory users at the top. Start by looking at the rows with the most bytes, and watch out for your own application classes rather than just generic ones like byte[] or String.

solid answer

~50 s

`jmap -histo` prints a numbered table sorted by total bytes descending. Each row has: rank (#), instance count (#instances), total bytes (#bytes), and the class name. The top rows are usually low-level types — `byte[]`, `char[]`, `java.lang.String`, `java.lang.Object[]` — because everything is built from them, so high counts there are normal. The investigative skill is to scan past those for *your own* domain classes (e.g., `com.acme.OrderLine`) appearing with surprisingly high counts or bytes; those are the actionable suspects, since arrays and Strings are usually *retained* by some application object higher up. Use `:live` to count only reachable objects, and compare two histograms over time: the class whose count or bytes grow monotonically is the leak candidate. Remember the histogram is flat — it tells you which class is large, not which object retains it; confirm ownership with a heap dump in MAT.

go deeper

for a junior

Can identify the instance-count and bytes columns and that the table is sorted by memory used.

for a middle

Reads array descriptors, uses :live, scans past low-level types to application classes, and diffs histograms over time.

for a senior

Distinguishes shallow vs retained size, explains why top rows are usually symptoms not causes, and pivots to a dump for ownership.

for a principal

Builds histogram-diff monitoring into ops, sets thresholds/alerts on class growth, and standardizes the triage-to-dump escalation path.

## The output format Running `jmap -histo <pid>` (or `-histo:live` to count only reachable objects after a GC) prints a table like: ``` num #instances #bytes class name (module) ------------------------------------------------------- 1: 2,184,003 174,720,240 [B (java.base@21) 2: 1,950,114 46,802,736 java.lang.String (java.base@21) 3: 512,008 40,960,640 com.acme.OrderLine (app) ... total 9,310,442 512,034,880 ``` Column by column: - **num** — rank, sorted by `#bytes` descending (biggest memory user first). - **#instances** — how many live objects of that class exist. - **#bytes** — total memory those instances occupy (shallow, i.e., the objects themselves, not what they reference). - **class name** — the class. Array types use JVM descriptors: `[B` = `byte[]`, `[C` = `char[]`, `[I` = `int[]`, `[L...;` = an array of objects. - **total** — grand totals at the bottom. ## How to read it 1. **The top is usually low-level types.** `[B` (byte arrays), `[C`/`String` (text), `Object[]`, `HashMap$Node` — these are the building blocks of everything, so big numbers there are expected and rarely the *root cause*. A `byte[]` is just memory; something else *holds* it. 2. **Hunt for your own classes.** Scan for application package names (`com.acme.*`). A domain class with an unexpectedly high instance count or byte total is the actionable suspect, because it is typically the *owner* that retains all those arrays/Strings. 3. **Use `:live`.** Without it the table includes not-yet-collected garbage; `:live` runs a GC first so counts reflect genuinely retained objects. 4. **Compare over time.** A single histogram is a snapshot. Take two (or more) minutes apart and diff: the class whose **instance count climbs monotonically** is leaking. This is the single most useful technique with histograms. ## The histogram's limit It is **flat**: it tells you *which class* is large but not *which object retains it* or the reference chain keeping it alive. `#bytes` here is **shallow size** (the object alone), not **retained size** (everything it keeps alive). To find the owner — the small static map or cache holding millions of entries — you must take a heap dump and use Eclipse MAT's dominator tree and path-to-GC-roots. The histogram triages; the dump diagnoses. ## Modern equivalent `jcmd <pid> GC.class_histogram` produces the same table on current JDKs.

  • The top row is `[B` with the most bytes — is byte[] your leak?
    Usually not directly. `[B` (byte arrays) underlie Strings, buffers, and many caches; it is almost always *retained* by some application object. Treat it as a symptom and look for the domain class or cache that holds those arrays, confirming ownership with a heap dump.
  • What does the #bytes column actually measure?
    Shallow size — the memory of the instances of that class themselves, not the retained size (everything they keep alive). Finding the true memory owner needs retained size, which only a heap dump analyzer computes.

saying these in an interview costs you the question

  • Concluding byte[]/String/Object[] at the top is the bug — they are building blocks usually retained by your own classes
  • Reading #bytes as retained size — it is shallow size; the real owner needs a dump's retained-size view
  • Acting on a single histogram instead of diffing two over time to confirm growth

context