Aleksey Shipilëv, a JVM performance engineer, writes detailed posts on JVM internals, the Java memory model, benchmarks and optimizations.
HN commenters call the JVM Anatomy posts a gem they learned a lot from, and say he explains JVM internals and performance very well.
Here are some software engineering blogs from my RSS reader in addition to the ones listed in TFA. I honestly don't know how over or underrated they are, but I guess that varies depending on which communities you're closer to.
Mikola Lysenko https://0fps.net
Graphics programming, geometry and algorithms.
---
Steven Wittens http://acko.net
Not underrated in any sense of the word, but I have to include it in case there are people who haven't seen it yet. It's about math and graphics, but also about art and design. Amazingly educational.
http://acko.net/blog/how-to-fold-a-julia-fractal/ is a good starting point for new readers.
---
Mark Nottingham https://www.mnot.net/blog/
Networking and HTTP.
---
Jeff Preshing http://preshing.com
Efficient concurrent data structures.
---
Bruce Dawson https://randomascii.wordpress.com
Software efficiency, performance optimization, fractals and more.
---
Yann Collet http://fastcompression.blogspot.com
Modern compression algorithms.
---
Gil Tene http://stuff-gil-says.blogspot.com
Also definitely not underrated, at least not in the Java world. But any engineer who cares about latency and honest benchmarks will benefit. Low latency techniques, GC performance, lock-free programming.
---
Tony Finch http://fanf.livejournal.com
C and UNIX programming, and more.
---
Andy Wingo https://wingolog.org
Scheme, Compilers, JavaScript, and much more.
---
Aleksey Shipilёv http://shipilev.net
JDK internals, low-level optimization.
I don't think it was true even in 2023.
This sounds like tackling the problems of C++ in the early 2000s.
1. Casey Muratori also that DRY shouldn't doesn't have to result in non-performant code.
2. Smaller functions, functions that do one-thing: Modern compiler can inline those. There are some edge cases where inlining may make less efficient use of states and loops but I don't think that's a main problem nowadays. I also wouldn't say the extreme version of this idea (very small functions) is still popular. The strongest proponent of this was Uncle Bob, and the last time I've heard him speak about code, he said he now lets the LLM write everything and he only reviews the module hierarchy and maybe the modules' public interfaces.
3. Polymorphism instead of ifs and switches was a big fad in the late 1990s until the late 2000s and had some holdouts in the 2010s. It was only ever popular in the Enterprise Java and C++ world (and maybe in Enterprise Smalltalk, never hard). Overuse of runtime polymorphism widely considered bad form in newer static languages like Go and Rust and in most dynamic languages there was always a tacit understanding of "use mostly conditions, add polymorphism if you need extensibility".
In functional languages (or languages heavily influenced by functional programming like Rust, Swift and Kotlin[1]), the classic approach for the type of scenario in this example is to use a sum type, and run a safe exhaustive match/switch on all the variants.
4.…
> A failure can propagate in the same circumstances in a Rust program.
What do you consider failure? A result or a panic? Those are worlds apart.
> First, Rust has panics, which are exceptions.
That's not how exceptions work in Java. Exceptions are meant to be caught (albeit rarely). Panics are meant to crash your program. They represent a violation of invariants that uphold the Safety checks.
Only in extreme cases (iirc Linux kernel maintainers) was there a push to be able to either "catch panics" or offer a fallible version of many Rust operations.
The Rust version of Java Exceptions are Results. And they are "checked" by default. That said, Exceptions in Java are huge, require gathering info and slow as molases compared to returning a value (granted you can do some smelly things like raising static exceptions)[1].
[1] https://shipilev.net/blog/2014/exceptional-performance/
> Non-specific criticism. If your complaint is that exceptions don't appear in function signatures, you can design a language in which they do. The mechanism is called "checked exceptions"
And everyone hates checked exceptions. Because rather than having a nice pipe that encapsulates errors like Result, you list every minute Exception possible, which, if we followed the mantra of "every exception is a checked exception" would make, for example, Java signatures into an epic rivaling Iliad.
> Besides, in the real world, junior Rust programmers (and some senior ones who should be ashamed of…
It's really not hard to reproduce,
- vt1 locks lock1
- vt1 suspends on lock2
- n VTs attempt to initialize a singleton that requires lock1, so they all suspend within pinning class init.
- you release lock2.
- all platform threads are pinned, so vt1 can't run and you hang forever.
There is no lock inversion and progress would have been entirely possible with platform threads, even with just one.
It happened on our system because we have parallel streams that all access the same singleton at the same time, which is fairly easy to do (you have lots of parallelism, you have a map operation that needs a value from your singleton and that's it.)
The solution is to never suspend while in a static block, but it's hard to generalize because... any method may suspend, and there is no way to know that it will because of colourlessness. And also the singleton pattern is common, often involve accessing expensive resources or IO (and suspending) and doing so with class init lock is recommended and common,
https://shipilev.net/blog/2014/safe-public-construction/#_safe_publication
In our case this involves scala's object {} which is a singleton defined using class init. Kotlin probably works the same way.
Most of these really don't matter as soon as you are using a JIT, and some that seem entirely obvious are much more problematic than you might hope.
Final field values can be changed through reflection and some other methods. The Java Language Specification allows a compiler/VM to ignore changes to final fields, but several libraries require them to be seen. So the that can't be optimised at the bytecode level.
The JIT can optimise final field access for some internal classes, and others if an option is set (see https://shipilev.net/jvm-anatomy-park/17-trust-nonstatic-final-fields/) and in Truffle based languages like TruffleRuby (https://github.com/oracle/truffleruby) we can tell the JIT that certain things are truly constant, but this can't be done universally for Java.
If you're producing bytecode and you want it to run fast then your best bet is to produce bytecode that is similar to javac's as that's what Hotspot has been tuned to run fast, even if it doesn't look optimal.
> Thanks for the answer. But is this actual behaviour for the GCs of the JDK? I was certain that at the very least Hotspot makes use of stack allocation as much as possible.
Not really, java has some escape analysis but it's very limited in its ability to stack allocate as it can't put entire structures (objects) on the stack, it only works if the compiler manages to scalar-replace the object (https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/) and that has somewhat restricted applicability (https://pkolaczk.github.io/overhead-of-optional/). The behaviour I'm talking about is mostly that of Go, as it is much more capable of stack allocating, and specifically has a non-generational GC because in testing they found generational GCs had a very variable impact depending on workload (rather than a universally or near universally positive impact).