In the early 1990s, software was written largely in C and C++, which give you speed and direct control but tie a compiled binary to one CPU architecture and one operating system. A program compiled for one machine often simply could not run on another.
That was especially painful in the era Java was designed for, where consumer devices shipped with different processors and the expectation was that software would move between them. The developer reality was that porting meant recompiling, and for anything touching the operating system, rewriting the platform-specific parts.
| Problem with native compilation | Why it hurt |
|---|---|
| Tied to one CPU architecture | A binary for one processor rarely runs on another |
| Tied to one operating system | Calls into the OS differ per platform |
| Manual memory management | Leaks, double frees, use-after-free |
| Raw pointers | Buffer overflows and segfaults |
| Platform-dependent type widths | long was not the same size everywhere |
Java's answer was to insert a virtual machine between your source code and the hardware, so that the part that talks to the CPU is a single well-tested component per platform rather than something every application has to get right.