- Levels of abstraction
It can be helpful to look at today’s programming languages across 3 different levels — “build fast” programming languages used for prototyping applications as quickly as possible, “infrastructure” programming languages which help with performance-centric/use-case specific portions of your application and finally, “systems” programming languages useful for embedded hardware and use cases where you need absolute control of memory usage. Of course, lines along such levels tend to be a bit more blurred and muddy in reality, but it helps us form a mental map of how to approach problems! Recommendation: if you’re just starting out with developing applications, “Build fast” programming languages are highly recommended. They get you to see the end result of whatever you want to build, faster.
“Infrastructure” programming languages — you can lump JVM-based languages (Java, Kotlin, Scala, Clojure, etc) as well as binary-producing languages like GoLang, Swift and Haskell into this camp. These languages tend to target performance in exchange for conveniences (strong typing, less components out of the box from the standard library, “heavier” verbosity, etc) without losing too much as well as impose rules to make getting things wrong, harder. They tend to be more manually tunable based on your execution environment (by allowing you to pass arguments that can affect runtime code performance). If you have a piece of your application that needs performance boost — especially if it’s running on the web, consider one of these for extra mileage.
“Systems” programming languages — you can lump C, C++ and Rust into this camp. These languages provide you the most control for your application and explicit memory handling when need be. They also tend to work well with embedded devices (programmable micro controllers, computers with non-standard processor architectures) and hardware without much software support (e.g. accessing your car’s information through its OBD port). With the rise of WebAssembly, such “low-level” languages also prove useful for performing computation-intensive work in support of web applications.
- Features and maturity
Syntax and data structures — languages serve as communication tools between a computer and a programmer. Take advantage of the language’s syntax. Know the most frequently used data structures in a language as well as their underlying implementation’s time complexities for insertions/deletions/modifications.
Runtime environment — be familiar with how your application “works” with respect to your computer. Does it need a language interpreter (the likes of Python, NodeJS, PHP)? Does it produce an architecture-specific binary (the likes of Swift and GoLang)? Does it use a mix of both — compiled and ran on some virtual machine (the likes of Java, Scala, Clojure)? Due to needs like these, learning and using Docker is highly recommended as well — you’ll learn a bunch about Linux administration along the way.
Libraries and maturity — Each language fits well for certain use cases based mostly on the kinds of projects their surrounding communities espouse. Java excels well for a lot of orchestration and network logistics-based requirements — database support through JDBC interface standardization and projects like those that fall under the Apache Foundation help Java serve that purpose. The same goes for Python and data analysis and statistics, as does Haskell with grammars, regular expressions and compilers. A language’s adoption rate and community size is also a great indicator of whether one should espouse a project under such a language — smaller language communities mean less help from the outside world when something breaks.
Garbage collection is the act of the program reclaiming memory space on its own, without the developer explicitly doing so (as you would in “systems languages” like C and Rust). In programming languages like Python, PHP and Swift, determining whether to deallocate objects or not are based on the count of references — the idea of reference counting. However, even while sharing this similarity, they differ on their respective implementations — specifically, with respect to how they handle classical memory leaks (objects with no useful references to them from the outside world but still prevent the garbage collector from cleaning them up!).
Python implements reference counting alongside a “stop-the-world” generational garbage collector — ”stop the world” as the garbage collector pauses program execution, kicks in and proceeds to do garbage collection, then resumes program execution and “generational” as the garbage collector mantains 3 separate “generations” — 3 sets of heaps. Generation 0 heap gets checked the most and contains the most “fresh” objects, followed by Generations 1 and 2.
PHP (since PHP5.3) implements a concurrent garbage collector alongside reference-counting, which runs alongside program execution if need be and is localized — not needing to traverse the entire memory space to search for cyclically referring objects by constructing a reference graph. Subgraphs which cannot be reached from the root can be safely eliminated.
Swift also uses reference counting but with no other garbage collection mechanism alongside it, leaving it up to the developer to ensure that objects which cyclically refer to each other are cleaned up through language primitives. Below, we demonstrate the use of a weak pointer — when an object’s “strong” reference count goes all the way down to 0,
Person will be cleaned up (as it is only weakly referred to by
Apartment). This allows Swift to add compile-time decisions as to when and where to garbage collect code, allowing it to save itself for having an actual runtime garbage collector.
There are lots of other garbage collection mechanisms implemented by other programming languages. They can widely affect the performance of an application depending on the use case so understand the memory model of your choice programming language for your use case!