The death of Java and .NET VMs
When I first started tinkering with Java in 1996, I was less than impressed at how sluggish the JVM was and how Java Applet-powered sites would be crippled by an annoying, CPU-hogging, grey rectangle. But, it was a miracle nevertheless. I was running Linux and Windows on x86, my job’s machine was a DEC Alpha (RISC) and some friends were Mac users (68k and PowerPC). Java ran in all of these platforms; so did Perl scripts, of course, but that’s another story.
Eventually Java got its game together by around version 1.3 and Microsoft caught up soon after. Today, both the JVM and the .NET CLR are considered “battle tested” and no one argues about their speed and fitness for most business applications. But here is the thing; the computing landscape looked much different in the 90s than it does now. Linux was not the de facto platform for running server-side workloads. There were a number of, rather different, competing platforms: Windows NT, commercial UNIX distributions (Solaris, SCO, Digital UNIX, AIX, etc.) plus AS/400, VMS, and so many others. CPUs also differed widely. There was x86, of course but there were also the likes of PowerPC and SPARC.
Fast forward to 2018 and both the desktop and server camps are pretty much under the total dominance of the x86 processor—yes, in its AMD64 edition, I know. When you download an application such as Microsoft SQL server, you are offered the choice of Linux, Windows or MacOS, but this is an OS choice rather than a CPU architecture one. Only in the mobile and IoT space we usually find a different architecture: ARM. What we see here is that, for most intents and purposes, the universal byte code of today is x86 machine language rather than Java byte code. This is how Docker images run on non-Linux platforms (Windows and MacOS).
We also have the microservices hype on our neck. If the most radical factions of the microservices “revolution” are to be believed, a microservice should perform exactly one function or task. If this is the case, bringing over an elephant-sized software layer like a VM alongside every single service seems overkill. It is easy to see that running 100 VMs to support 100 microservices on a single host is not ideal whereas running 100 micro services written in Go or Rust, each taking a handful of MBs does not look like a problem at all. The devil’s advocate view is that developers are expensive, and machines are cheap: point taken.
Getting back to this post’s argument, the question is: should we still take VMs as necessity in the age of Docker? These are the first reasons as to why I could justify the use of VM-based languages in 2018.
Legacy investments: If we are in an organisation that has substantial amounts of code written in Java or C#, and the cost of rewriting the stack would pay for a 500 node Kubernetes cluster for the next three centuries, then, sticking to a VM-based language is a no brainer.
Rich debugging capabilities: In optimised, compiled languages, rich symbolic information might not be included in the final binary. Depending on the specific language, hot-debugging capabilities may be limited, unlike the VM world were variable names and method names are typically preserved and developers are spoiled with the likes of appDynamics. Also, the VM byte code typically preservers the structure of the original source code whereas a final native machine language binary me lack the original abstraction—for example, loop unrolling optimisations.
Actual language features: Antagonising the VM paradigm based on compute resource consumption is, of course, rather myopic. Languages like F# in the .NET world and Clojure in the JVM are extremely productive when used in specific contexts such as DSLs. They also benefit from a rich library eco-system which is lacking in alternatives compiled languages like Haskell and Scheme.
My conclusion is that if we buy into the Talibanesque view on microservices (one function per service), then VMs are indeed a burden. But I have mixed feelings about ignoring 30 years of language research and settling for a compiled language like Go that favours fast compile and learning time at the expense of safe language primitives. Rust, in this sense, has more modern features like algebraic data types and it has not got the burden of a garbage collector; however, its steeper learning curve and the skill involved in manual memory management are two factors that must be considered.