DEX files and the 64K limit
DEX packs every class in your app into one file with shared tables. That design is why an APK is small, why a single file has a 65,536-method ceiling, and why multidex exists.
A Java compiler produces one .class file per class, each with its own constant pool. Android ships
one file for the whole app. That single decision explains the format's strengths, its limits, and
most of the build-tooling complexity you have ever encountered.
TL;DR#
- A DEX file holds many classes with shared string, type, method and prototype tables. The same string used by five classes is stored once.
- The bytecode is register-based, not stack-based, which suits the interpreter Android was designed around.
- A DEX file indexes types and prototypes with 16-bit fields, and method references similarly. The practical ceiling is 65,536 method references per DEX file, which includes framework and library methods you call.
- Multidex splits the app across
classes.dex,classes2.dexand so on. ART loads them natively from Android 5.0; older versions needed a support library. - The format keeps evolving — new opcodes and new versions — without breaking old apps.1
Mental model#
Think of a printed dictionary versus a stack of index cards.
A stack of cards is easy to write one at a time and easy to reorder, but every card repeats the words it refers to. A dictionary shares one definition per word and uses cross-references. It is denser, harder to edit, and has a fixed number of entry slots per volume.
DEX is the dictionary. javac produces index cards; d8 compiles them into one volume with shared
entries. That is why an APK is much smaller than the equivalent set of class files, and why you
eventually need a second volume.
Where the analogy breaks#
A dictionary's size limit is physical. DEX's 65,536 limit is an encoding choice: the index fields are 16 bits wide. It is a format decision, not a property of the code, and it is why adding a library can push an app over the edge without adding much code of its own.
The big picture#
Text description of this diagram
A six-stage pipeline from Kotlin source to running code. kotlinc and compiler plugins produce class files on the build machine. R8 and desugaring optimise and rewrite them. d8 produces DEX files. At install time dex2oat compiles the methods named in a profile into oat and vdex files. At runtime the JIT compiles hot methods that were not precompiled, and during idle maintenance dex2oat recompiles using the profile collected at runtime. Each stage is labelled with where it happens and what it produces.
The diagram puts DEX in context: it is the build machine's output, not the phone's. By the time the app runs, the code has usually been through at least one more compilation step.
Deep dive#
What is actually in a DEX file#
A DEX file is a header followed by a set of tables and a data section. The tables are the interesting part, because they are what makes the format compact:
| Section | Holds |
|---|---|
string_ids | Every string in the file, sorted, no duplicates |
type_ids | Every type, referenced by string index, sorted |
proto_ids | Method prototypes: return type plus parameter types |
field_ids | Every field referenced, defined or not |
method_ids | Every method referenced, defined or not |
class_defs | The classes defined in this file |
The critical word in the last two rows is referenced. method_ids contains every method the
file calls, including Object.equals, StringBuilder.append and everything from the Kotlin
standard library. That is why the limit is easy to hit: your dependency graph counts against it, not
just your own code.
Register-based bytecode#
Dalvik and ART execute a register-based instruction set. Where the JVM's bytecode pushes operands
onto a stack, DEX instructions name registers directly: add-int v0, v1, v2 rather than load, load,
add, store.
The trade-offs:
- Fewer instructions per operation. One DEX instruction can replace four JVM instructions.
- Larger instructions. Each operand needs register bits, so the encoding is wider.
- Fixed frame size. A method's frame is sized at entry, and arguments occupy the last registers of the frame.
Registers are 32 bits wide; 64-bit values use adjacent pairs. Many opcodes can address only the first
16 registers, some reach 256, and the move family can reach 65,535. When a method needs a register
outside an instruction's range, the compiler inserts a move to a low register first.2
That last detail is a good example of how the format shapes generated code: the register file is large, but the instruction encoding is not uniform, so the register allocator has to work around it.
Versions and magic#
A DEX file starts with a magic number that encodes a three-digit version: dex\n035\0, dex\n039\0
and so on. The version increases monotonically, and each increment corresponds to new opcodes or new
header fields.
| Version | Introduced | What it added |
|---|---|---|
| 035 | Early Android | The base format |
| 037 | Android 7.0 | Default methods, adjusted invoke |
| 038 | Android 8.0 | invoke-polymorphic, invoke-custom, method handles |
| 039 | Android 9.0 | const-method-handle, const-method-type |
| 040 | Android 10 | Extended SimpleName characters |
| 041 | Android 16 (experimental) | Container format |
The container format in 041 is worth a note: it lets several logical DEX files live in one physical file, which is a response to a real problem — a single large DEX is slow to verify and to load. It was experimental in Android 16 and not intended for production code at that time.1
The 64K limit in practice#
The build error is unmistakable:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.or
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536Both mean the same thing: a single DEX cannot hold that many references. The fixes, in order of preference:
- Remove code. R8 shrinking, dependency review, and replacing a large library used for one utility method.
- Enable multidex. More DEX files, each within the limit.
- Restructure. Split a feature into a dynamic feature module so it is not in the base APK.
Multidex is not free. The build has to decide which classes go into the primary DEX, which makes builds slower; on older Android versions the extra DEX files had to be installed at startup, which could cause an ANR if they were large.3
Multidex and the primary DEX#
The primary DEX is special: it is the one the runtime loads first, so anything needed during
Application startup must be in it. If a class used before MultiDex.install() completes ends up in
a secondary DEX, you get a ClassNotFoundException at launch — a failure that only appears on the
older devices you were trying to support.
The tools expose a keep rule for this: classes listed in multiDexKeepProguard are forced into the
primary DEX. It is a blunt instrument, and using it heavily is a sign that the app's startup path
depends on too much code.
DEX versus class files#
Why not just ship .class files?
- Size. Shared tables across an entire app beat per-class constant pools by a wide margin.
- Startup. One file to open, verify and map instead of thousands.
- Verification. ART verifies a DEX as a unit, with a format designed for it.
- Obfuscation. R8's output is a DEX-shaped graph, and shrinking works better on the whole program than per class.
The cost is a global namespace with a hard ceiling, and a build step that must reconcile every dependency into one numbering scheme.
There is a second, subtler difference: the constant pool is global. In a .class file, each
class has its own pool, so two classes using the same string store it twice. In DEX there is one
string_ids table, and every class refers to it by index. An app with ten thousand classes and a
large shared vocabulary of identifiers saves a substantial amount of space, which is exactly the
regime a typical Android app is in.
The trade-off shows up in tooling. A per-class format lets you compile, shrink and analyse one class at a time. DEX forces whole-program tools — R8, D8, dexlayout — because the indices only make sense after every class has been assigned a slot. That is why building an Android app is a whole-program operation in a way that building a plain JAR is not.
The build steps that produce it#
The path from source to DEX is worth knowing by name, because each step exists for a reason:
- kotlinc / javac produce
.classfiles. - Desugaring rewrites language features that the target API level does not support natively into equivalent older constructs.
- R8 shrinks and optimises: it removes unreachable code, inlines, and rewrites names.
- D8 converts the result to DEX.
- Packaging puts
classes.dexand any additional DEX files into the APK.
D8 and R8 are separate tools with overlapping jobs. D8 is the dexer; R8 is the optimiser. In a modern build they run in sequence, and R8's output is what D8 converts. The names in your stack traces are R8's output, which is why mapping files exist.
How it evolved#
The interesting arc is not the format itself but what surrounds it: multidex became native in 5.0, D8 replaced dx, and R8 took over shrinking and optimisation. The file format changed slowly; the toolchain around it changed completely.
Under the hood#
- dex_file.hthe in-memory model of a DEX file
- dex_file_loader.ccversion detection and container handling
- dex-format.htmlthe historical format specification — superseded by the AOSP docs page
- DexFile.javathe API that loads DEX files
Curiosity corner#
- A "method reference" includes methods you never call directly. Every transitive dependency counts toward the 65,536.
- The magic number is a version, not a checksum.
dex\n039\0tells the loader which opcodes are legal. - Register-based bytecode is why DEX instructions look strange.
invoke-virtual {v0, v1}, ...is the format naming its operands directly.
Try it#
adb shell pm path com.example.app`pm path` prints APK paths, not DEX names. Unzip the base APK to list its classesN.dex files.
unzip -p app.apk classes.dex | head -c 8 | xxdThe version digits depend on the app's minimum API level; 039 is one example.
Check yourself#
Check yourself
How is a new app process created from Zygote?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsDalvik Executable (DEX) formatsource.android.comaccessed 2026-10-27↩
- docsDalvik bytecodesource.android.comaccessed 2026-10-27↩
- docsEnable multidexdeveloper.android.comaccessed 2026-10-27↩
- docsEnable code shrinking with R8developer.android.comaccessed 2026-10-27↩