Zygote
Zygote, Dalvik & ARTintermediate

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.

12 minAPI 1–37 · 17interactivequizverified 2026-10-27

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.dex and 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#

Loading diagram…
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:

SectionHolds
string_idsEvery string in the file, sorted, no duplicates
type_idsEvery type, referenced by string index, sorted
proto_idsMethod prototypes: return type plus parameter types
field_idsEvery field referenced, defined or not
method_idsEvery method referenced, defined or not
class_defsThe 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.

VersionIntroducedWhat it added
035Early AndroidThe base format
037Android 7.0Default methods, adjusted invoke
038Android 8.0invoke-polymorphic, invoke-custom, method handles
039Android 9.0const-method-handle, const-method-type
040Android 10Extended SimpleName characters
041Android 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]: 65536

Both mean the same thing: a single DEX cannot hold that many references. The fixes, in order of preference:

  1. Remove code. R8 shrinking, dependency review, and replacing a large library used for one utility method.
  2. Enable multidex. More DEX files, each within the limit.
  3. 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:

  1. kotlinc / javac produce .class files.
  2. Desugaring rewrites language features that the target API level does not support natively into equivalent older constructs.
  3. R8 shrinks and optimises: it removes unreachable code, inlines, and rewrites names.
  4. D8 converts the result to DEX.
  5. Packaging puts classes.dex and 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#

Versions8 releases
APIVersionReleasedWhat changed
11.02008-09-23—
82.2 Froyo2010-05-20—
215.0 Lollipop2014-11-03—
247.0 Nougat2016-08-22—
268.0 Oreo2017-08-21—
289 Pie2018-08-06—
29102019-09-03—
3717 Cinnamon Bun2026-06-16—

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#

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\0 tells 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#

Count DEX files in an installed app
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.

Inspect a DEX header
unzip -p app.apk classes.dex | head -c 8 | xxd

The version digits depend on the app's minimum API level; 039 is one example.

Check yourself#

Check yourself

1 / 12

How is a new app process created from Zygote?

0 answered · 0 correct

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.

  1. docsDalvik Executable (DEX) formatsource.android.comaccessed 2026-10-27↩
  2. docsDalvik bytecodesource.android.comaccessed 2026-10-27↩
  3. docsEnable multidexdeveloper.android.comaccessed 2026-10-27↩
  4. docsEnable code shrinking with R8developer.android.comaccessed 2026-10-27↩