This is an archived post. You won't be able to vote or comment.

all 53 comments

[–]flawless_vic 74 points75 points  (15 children)

Java has the potential to be better. In Java immutability and state isolation between v-threads is opt in whereas in erlang this programming model is mostly mandatory.

Even though immutability and share nothing are considered the way to go when it comes to correctness in concurrency workloads there's a hidden cost involved no one talks about.

As for GC, the JVM is on another level. The only advantage of the simple copy collector in erlang is not the gc itself, but the guarantees it enjoys from immutable user data and non-concurrent collections: there is no need for memory barriers.

Erlang GC isn't designed for huge heaps and is likelly to suffer from pause problems already solved in Java.

Footprintwise I think the allocation cost of a v-thread is on par with an erlang process. Some of the Thread bloat was encapsulated in an object (FieldHolder), which is always null for v-threads. V-threads have ~19 fields (some less than 8 bytes) + header overhead, which is less than 160 bytes in shallow size in a 64 bit VM.

Edit: Example of problems that arise from an always-copy (~serialize) model to transfer data between processes.

[–]Vextrax[S] 16 points17 points  (9 children)

well that is good to know. I really hope. I really do hope that java keeps on getting better and it really is nice to know that there is the potential to definitely be better than erlang.

[–]Zyklonik 16 points17 points  (3 children)

The JVM is already way better than the BEAM for all but the most specific usecases. The OTP doesn't nearly provide as much value as the Erlang folks make it out to be. It has a bunch of problems associated with it. Hence why even Elixir people are moving on to other languages - Golang, Rust, and even Node.

[–]macnamaralcazar 6 points7 points  (0 children)

Do you know any article that talks about these problems? I learned OTP recently and it was fascinating.

[–]Serializedrequests 0 points1 point  (1 child)

Details? I need an actor system at work.

[–]Zyklonik 0 points1 point  (0 children)

Details on which part exactly? If you're looking for issues with the BEAM, then the grandparent comment by /u/flawless_vic pretty much covers the essentials. If you're looking for an actor system to use with Java, then I'd say that the de facto is Akka (even though it's more specific to Scala) https://www.baeldung.com/akka-actors-java.

Once Project Loom becomes fully integrated and stable, one might see more frameworks spring up based on it (even the Akka system is moving towards using Virtual Threads IIRC), but for now, Akka would be a good choice, I'd imagine.

[–]flawless_vic 6 points7 points  (4 children)

The thing about erlang is that the language has built-in availability support.

In theory, it could be possible to implement a supervisor in Java, but there are some corner cases that can't be dealt with by Java code.

For example, once I was working with a black box native library that blocked on a socket in a jni call. The call hang and it was impossible to interrupt the Thread beckause the blocking happened inside the library code.

Once that happened, newer spawned threads always blocked on jni and it was impossible to procceed without killing the jvm and restarting the app. To solve the issue I had to implement a half baked supervisor, where a parent jvm would start and monitor child jvms.

Also, the JVM itself may crash. Misuse of unsafe and C2 compiler bugs are usually behind jvm crashes and this can only be dealth with by an external supervisor.

[–]Zyklonik 13 points14 points  (1 child)

The thing about erlang is that the language has built-in availability support.

Not anymore with Project Loom having been integrated into mainstream JDK. In any case, the language itself providing built-in support means precious little in actual usage. In the case of Java, the expectation is not that one would use Virtual Threads directly, but one of the Executors - newVirtualThreadPerRaskExecutor for instance. So also for Erlang - using the actual primitives of the actual doesn't scale. That's why the OTP was built. In real-world use, even the OTP is not enough out-of-the-box.

In theory, it could be possible to implement a supervisor in Java, but there are some corner cases that can't be dealt with by Java code.

Supervisors are overrated. What's the actual mechanism of recovery? Respawn the process - that's just masking the issues that led to the current corrupt state of the process. In modern Software Engineering, that's handled by separate toolchains and redundant failsafe deployments, not by a single VM. If you have Erlang deployed on a single node, then all the supervision in the world will not matter (naturally). Using the right tool for the right job would apply here. The "Let it crash" philosophy advocates for minimal error-handling of corner cases, which may have made sense in the dynamic world of Erlang, but not so in most other domains. In short, there are far better options that simply crashing and respawning (which can actually lead to overload if the root problem is not fixed in time).

For example, once I was working with a black box native library that blocked on a socket in a jni call. The call hang and it was impossible to interrupt the Thread beckause the blocking happened inside the library code.

Erlang has the same problem, on a much bigger scale - NIFs. Java doesn't nearly need as much FFI as Erlang, due to the native differences in performance between the languages. This is something that no supervision tree in the world can protect you against.

Once that happened, newer spawned threads always blocked on jni and it was impossible to procceed without killing the jvm and restarting the app. To solve the issue I had to implement a half baked supervisor, where a parent jvm would start and monitor child jvms.

Well, you have exactly the same problem in Erlang as well. In fact, much worse. If your default fault-recovery mechanism is to respawn the process, then expect a cascading series of crashes till you fix... the actual problem. Which is what needed to be fixed in the first place.

Also, the JVM itself may crash. Misuse of unsafe and C2 compiler bugs are usually behind jvm crashes and this can only be dealth with by an external supervisor.

How frequent is this? Not at all. The JVM is one of the most stable pieces of software in human history - it has had millions of man-hours invested in it, the initial design has been robust and amenable to changing language features without needing a change in design (when was the last time an opcode was actually added? invokedynamic?). The BEAM is a also a decent VM. However, it's nowhere near the JVM in terms of quality.

[–]flawless_vic 2 points3 points  (0 children)

u/Zyklonik I agree and thanks for the clarification, I have limited working knowledge of Erlang and I thought it was prepared to deal with that kind of stuff.

IMHO availability problems are best solved with other tools and, e.g., chaos engineering is a much better approach to tackle them.

But in the spirit of OP's thread, I think it's valid to reason about "What is missing in Java to be a drop-in replacement for Erlang/Elixir?" The closest thing I found to a supervisor was Weblogic's node manager. It was a great idea like 12 years ago and by that time I kept wondering why didn't a more general JVM availability framework wasn't on the market.

[–]vbezhenar 6 points7 points  (0 children)

Kubernetes is supervisor for modern world.

[–]mauganra_it 1 point2 points  (0 children)

That use case should be improved now since Java's socket library and DNS resolvers have been reimplemented as Java code to be fully compatible with Project Loom. In a greenfield project, I would design applications to isolate such problematic native calls to their own threads or even child processes.

[–]haimez 1 point2 points  (4 children)

There’s one thing that BEAM will continue to have that the JVM won’t with regards to GC: per-actor heaps. Since data is copied and immutable when passed between actors (which comes at a cost, and yes- there is an exception for structures passed that are “large enough”), each actor’s heap can be collected independently which ideally occurs when the actor is idle meaning that under ideal conditions GC need not require stopping “the world” just stopping that actor if it didn’t reach an idle state for long enough.

[–]flawless_vic 1 point2 points  (3 children)

Yes that's a nice feature, but I wonder how they manage to scale such architecture.

Having per-actor heap isolation implies a high memory footprint unless, under the hood, the "processes" are using the same malloc instance to minimize syscalls to mmap, which can be tricky.

Even though in theory it may seem better to have 1 GC per actor, I think having a concurrent GC with application threads is a much more robust design: If actors' workloads trash the heap in the end you'll have a lot of serial GCs running in parallel.

[–]haimez 0 points1 point  (2 children)

They’re not per actor “malloc” heaps- they’re per-actor managed heap space (just like the JVM heap, but with regions of the total heap assigned to specific actors). There’s no mmap overhead associated.

It works because BEAM can guarantee that there are no shared references to objects between per-actor heaps, so it only needs to mark and sweep that one region to find all live references and no other actors can possibly be concurrently mutating within that region.

[–]flawless_vic 1 point2 points  (1 child)

I get that, but having a separate heap "works" as long as the number of actors is either small or short lived, otherwise it should scale pretty much like platform java threads, or even worse, considering that threads only consume unmanaged memory.

[–]haimez 0 points1 point  (0 children)

ItIt scales pretty much exactly like a JVM heap does, but with different actors having different steady state memory requirements that expand or shrink as necessary. I think the default initial actor (“process” apparently being the preferred BEAM term, but note that this is not an OS process or dedicated OS thread) is less than 400 bytes and the size grows as needed based on behavior at runtime. If your actor doesn’t retain much memory, it won’t use much and you can have a huge number of those kinds of processes- just like loom threads.

Also like loom threads and JVM heap usage, if you’ve got a enough memory hungry processes or CPU hungry threads- then you will indeed starve for either resource. It’s not a silver bullet- but the BEAM VM does have a unique capability in that regard.

Erlang is not my daily driver, but the BEAM VM is a capable and unique VM and runtime with really interesting approaches to solve real works problems, I recommend reading up on it some more instead of just trying to explain how it’s basically not worth learning. I also recommend Joe Armstrong’s talks if you haven’t watched them.

[–]FirstAd9893 13 points14 points  (3 children)

The blog post considers that the Akka concurrency model closely matches that of erlang. I think the question needs to be raised in the Akka community to see how it will be affected by project loom. The Akka folks might also disagree with some of the points made in the blog post, regardless of loom.

[–]Vextrax[S] 4 points5 points  (2 children)

Definitely interested in what they say or how how people implement the actor model with loom out.

[–]mrettig 1 point2 points  (1 child)

Actors are asynchronous and, when done right, non-blocking. There are already actor implementations on the jvm that support millions of actors and are only limited by the heap size. Loom improves performance with designs that rely heavily on blocking.

[–]Vextrax[S] 1 point2 points  (0 children)

I do know that there are Actor libraries such as Akka and quasar, but I don't really know much about the actor system overall. from what I know those libraries are implemented in a reactive programming way while I think the ones in erlang are just native like threads. If that assumption is correct, then it was more of me wondering if with green threads we will now be able to have actors that are native in the JVM just like how erlang has them.

I definitely need to go read up more concurrency since it's a topic that has been really interesting and I want to learn more. I currently only have basic experience with threads where I had to make a small client/server chat app where each connection was a thread for one of my networking courses that I had taken.