The post glosses over the most important part of Erlang's GC: it collects process heaps separately. This transforms a hard problem (collecting a global heap with low latency despite concurrent mutators) to a _much_ simpler problem, at the price of more copying. Compare Java's G1 with Erlang's GC; the former hurts my head.
For those problems that are amenable to Erlang's model, this is a fine solution. The only real improvement here would be making collection incremental.
Erlang also has reference counters for things like strings that are immutable and can be shared between threads (processes in Erlang).
Overall this is a good model. Use GC for small per green thread heaps. Then use reference counters for shared immutable structures that cannot form cycles and copy everything else.
Erlang only uses reference counting for binaries larger than 64 bytes, everything else is allocated on the process heap (or in heap fragments) and copied. Just that is enough to have a beneficial effect though, since large binaries are relatively common in practice, and are frequently passed around from process-to-process.
And if you have any other global state you want to pass around, you can pull off a clever trick by passing it around as a binary and then unpacking it as needed within caller processes.
AFAIK this trick is why BEAM files use an IFF-derived format (easy to parse individual chunks out at runtime), and why erlang:module_info/{1,2} are the way they are: working with module metadata literally just means asking the code-server process for the (shared refcounted) module binary, and then parsing it yourself.
I thought Erlang's Garbage collector was incremental by virtue of being per process.
A system may have tens of thousands of processes, using a gigabyte of memory overall, but if GC occurs in a process with a 20K heap, then the collector only touches that 20K and collection time is imperceptible. With lots of small processes, you can think of this as a truly incremental collector.
It's not incremental per process, but I'm not sure it would even matter that much in practice.
Yes, that is how it works, except (as you implicitly note) that large heaps in single processes can cause problems; allowing incremental collection per heap would flatten the latency profile further.
Large GC jobs get scheduled on dirty schedulers today (a background thread pool), since it's not OK to block a normal scheduler more than 1ms or so in Erlang. If they could be split into smaller chunks of work, perhaps it could be done on normal schedulers, making time allocation more fair.
Ten years ago was two whole technological generations ago in the implementation of OpenJDK's GCs. OpenJDK now has a maximum pause time of under 1ms for heaps up to 16TB.
I really strongly doubt that GC is a bottleneck for Erlang programs on either the BEAM or the JVM - the sophistication of the scheduler, and the way various language primitives interact with it, is where the BEAM is almost certainly gaining an edge over the JVM. That said, I'm sure there are a subset of programs that _would_ be faster on the JVM, just depends on what metrics are being compared.
> the sophistication of the scheduler, and the way various language primitives interact with it
That was brought over to the JDK six months ago. The JDK can now spawn millions of Erlang-like processes ("virtual threads") per second.
Erlang is a great inspiration and it does incredibly well with the development resources available to it, but it's hard to compete with the level of engineering investment in the JDK and its state-of-the-art GCs, optimising JIT compilers, and low-overhead in-production tracing and profiling.
The BEAM is so much more than just a green thread runtime - and it is not straightforward at all to just "bring it over" to another virtual machine, the entire BEAM VM is designed around the scheduler, and core features such as signals (used most notably for links/monitors, but also for a number of other system features) and messaging, are deeply integrated with the scheduler; as are system tasks, execution of NIFs (natively-implemented functions), and more. Furthermore, the schedulers are adaptive to the amount of work in the system, and the behavior of the other schedulers, i.e. they aren't just simple work-stealing queues for green threads. Over 20 years of engineering effort have been invested in this system, and it is highly optimized for the types of workloads that Erlang is used for.
In my opinion, trying to replicate ERTS on top of another virtual machine either requires making that virtual machine more like the BEAM, or will end up always playing catch up with the BEAM itself in some aspect. That's just my two cents though.
I am well familiar with BEAM, and my fascination with it ~20 years ago was one of the things that inspired me to become a VM engineer. Bringing that over to the JDK was, indeed, a large effort, but we are a large, well-funded team.
And, as a JDK engineer with Erlang experience, I'm just sayin' that as of the past six months, the JDK has all the primitive building blocks required for a very efficient Erlang runtime. Even the thread scheduler is not built into the JVM but written in Java, and will soon be made completely pluggable. Any scheduling algorithm that Erlang wishes to employ can be provided as a library on top of the JVM. If I've missed anything, we'll consider adding it.
It's been a dream of mine to port BEAM to the JDK to give Erlang much better performance and greater visibility. Unfortunately, this work can't be funded as there's insufficient demand for Erlang. If the Erlang community does it, however, it could increase its evolution velocity, improve performance, and give it higher visibility.
BEAM doesn't support mutation, the JVM does. that's a big plus in the BEAMs corner.
Also when two BEAM VM speak to each other its as if they are just one machine. no serialization de serialization of data, no protobufs, etc. It's all just primitives being sent between processes regardless of their location.
> BEAM doesn't support mutation, the JVM does. that's a big plus in the BEAMs corner.
The JVM supports languages with mutation or without. That x86 supports mutation doesn't hinder, say, Haskell from being efficiently compiled to it.
> Also when two BEAM VM speak to each other its as if they are just one machine. no serialization de serialization of data, no protobufs, etc. It's all just primitives being sent between processes regardless of their location.
There is no need for that protocol to be built into the VM . The JVM is a lower-level VM than BEAM (parts of the runtime, like the thread scheduler and even one of the JITs are written in Java).
I have the utmost respect for Erlang's design, and have borrowed inspiration from it. It is truly a towering achievement. But BEAM simply doesn't have anywhere near the level of investment required to be a leading platform. I wish there was more demand for it, but there isn't.
It's important to highlight some key differences between the JVM and the Erlang VM (BEAM) when it comes to accommodating language semantics, fault tolerance, and process isolation.
The JVM is designed as a general-purpose VM, aiming to accommodate a wide variety of languages with different semantics. This broad scope can sometimes lead to compromises when it comes to optimizing for specific language features or use cases. On the other hand, the Erlang VM is tailored to the semantics of Erlang and fault-tolerant systems, which allows it to be highly optimized for these specific requirements. This focused approach enables the BEAM to excel in areas such as concurrency, fault tolerance, and process isolation.
As mentioned earlier, the BEAM offers strong fault tolerance and process isolation. When a specific library or process encounters an issue and panics, it doesn't bring the entire VM down. Instead, only the affected process is terminated, allowing the rest of the system to continue functioning as expected. This level of isolation is achieved through BEAM's lightweight processes, which have their own heap and stack, and communicate with each other only through message passing.
Some features that the Erlang VM has, which are specifically tailored to fault-tolerant systems and not found in the JVM, include:
a) Per-process garbage collection: BEAM uses a per-process garbage collection approach, which enables it to efficiently handle millions of lightweight processes without the need for stop-the-world GC pauses.
b) Preemptive scheduling: BEAM's preemptive scheduler ensures that no single process can monopolize the CPU, and that all processes receive a fair share of processing time. This is a crucial feature for building highly concurrent and fault-tolerant systems. (The BEAM is ostensible an OS and can even be run bare metal!)
c) Supervision trees: The Erlang/OTP platform provides a built-in supervision tree mechanism, which allows developers to define strategies for monitoring and restarting failed processes automatically. This contributes to the overall stability and reliability of the system.
The JVM has made significant advancements in performance and garbage collection, the BEAM has been specifically optimized for the unique requirements of Erlang and fault-tolerant systems. Its focused approach allows it to offer features and optimizations that are particularly suited for these use cases.
In response to:
> But BEAM simply doesn't have anywhere near the level of investment required to be a leading platform. I wish there was more demand for it, but there isn't.
While it's true that BEAM may not have received the same level of investment as the JVM, it's important to note that the level of investment doesn't necessarily determine a platform's effectiveness or suitability for specific use cases. The Erlang VM has been designed and optimized over 35 years for the unique requirements of Erlang and fault-tolerant systems, which has resulted in a set of features and optimizations that cater specifically to these needs, its unparalleled in this field, and As such, the BEAM's focused approach allows it to be a strong contender in its niche, particularly for building highly concurrent, fault-tolerant, and distributed systems. It's not necessarily about competing with the JVM across the board; instead, it's about excelling in specific areas where the Erlang VM's unique features and optimizations provide tangible benefits.
Moreover, the Erlang/OTP ecosystem has a dedicated and passionate community that continues to drive its development and innovation. This community's efforts have ensured that BEAM remains a powerful and relevant platform for building resilient systems, despite having comparatively less investment than other platforms like the JVM.
OpenJDK's new GC doesn't require STW pauses, the JDK offers preemptive scheduling for millions of threads as well as supervision trees -- although, to be fair, these are recent changes that have, at least in part, been inspired by Erlang. All that is on top of one of the world's most capable compilers. I have long admired Erlang and its passionate community as well as how much they've been able to accomplish (and, as a VM engineer, studied its design), but between passion and being an actual top-tier competitor there's a certain reality gap.
To be a competitor, to be fair, implies there's competition. ¯\_(ツ)_/¯.
There's no world, where I'd allow for some future developer to write some Java on the same VM as my Erlang/Elixir code and risk the entire VM crashing.
JVM and Erlang's VM cater to different needs and programming paradigms. While they might share some features or learn from each other's innovations, they are not direct competitors but rather address unique use cases and development requirements.
People who want Erlang are going to reach for Erlang/OTP.
People who want Scala/Akka/Java-monolith will leverage the JVM.
Replatforming, doesn't happen often.
No one is hopping from one platform for the other.
What do you mean by "the entire VM crashing"? The JVM is no more susceptible to crashes than BEAM.
BTW, my point is that can now be a very good platform for Erlang/OTP. Its performance will allow Erlang/Elixir programs to reduce their reliance on native code and to handle higher loads.
It's my understanding the state of the art in observing JVM-based applications is a combination of using thread dumps, gc logs, thread activity visualizations. Thread dumps give us a snapshot of the the name of the thread, its current running state (waiting, blocked, etc), and the stacktrace of the work its currently doing. GC logs give you a record of when and how much garbage was collected and Thread activity visualizations show you the timeline of thread moving between different running states.
The BEAM gives you the ability to see the bottlenecks in your system, via the REPL (in real time!)
It has world-class introspection built in that gives you the power to observe and manipulate your running application through a REPL.
The BEAM has hundreds of features like this, because the BEAM is more of an OS than and VM.
I get it, you're a JVM expert, but the BEAM is more than a check list of optimizations that on paper the JVM can do.
I strongly suggest, before the next time you comment on an BEAM VM vs.JVM debate, please consider watching https://www.youtube.com/watch?v=JvBT4XBdoUE, "The Soul of Erlang and Elixir • Sasa Juric • GOTO 2019"
and reading https://github.com/happi/theBeamBook " an attempt to document the internals of the Erlang runtime system and the Erlang virtual machine known as the BEAM."
You can do all that with the JDK, too, and at higher performance.
As an Erlang fan for many, many years, I am quite familiar with BEAM's design (and why I think Erlang would gain a lot by running on the JVM (or, more precisely, from implementing BEAM and OTP on top of the JVM). Aside from implementation details -- the JDK has simply had a lot more work put into it -- the main difference between the two is that BEAM is higher level, and the runtime itself does more things out of the box, while the JVM is lower level and you can do more in libraries (with no loss of performance, because Java is compiled to very efficient machine code).
Not only would Erlang and Elixir gain better performance and be able to reduce application's reliance on NIFs, but maintaining and evolving them would require less effort.
There is now (as of JDK 19, released six months ago) no capability offered by BEAM -- including runtime manipulation of code through a REPL -- that couldn't be implemented at least as efficiently as a library on top of the JDK. You can see the API here: https://docs.oracle.com/en/java/javase/20/docs/specs/jvmti.h...
You’re moving the goal post. The JVM does provide real time observability into your application.
Personally I think in a terminal with just text is inferior to a proper gui tool. If you really want this jcmd can convert jfr data into different formats that you then can script against.
It's the actor model all the way down... that's the abstraction.
Simple systems, are easy to manage, complex systems are harder to manage, hence why you need a full blown GUIS to manage the JVM, because it does everything, and tries to satisfy every use case, so it has to handle every use case.
The author of the article you linked is about a decade out of date. "The state of the art in observing your JVM-based applications is a combination of using thread dumps, gc logs, thread activity visualizations." -- that has been untrue for quite some years now. The basic observability offered by the JDK is now the Flight Recorder, a low-overhead in-production tracing and profiling engine that BEAM is many years away from being able to achieve.
The JVM supports all the same runtime observation and manipulation as BEAM. If you want to use them via a shell -- you may. If you want to use the actor model "all the way down" you can do it. The JVM is simply a virtual machine at a lower level than BEAM, but it does provide all the primitives the Erlang model needs.
The reason I'm telling you this is that I think that targeting the JVM would allow Erlang/Elixir to evolve faster -- enjoying the low-level work done by the large OpenJDK team -- as well as reach a wider audience. At the same time, the wider Java platform community will be exposed to all the great ideas that have come out of Erlang. All the necessary foundational work is now, finally (as of JDK 19), in place.
It is a goal post move. You went from "Beam provides real time observability" to "real time observability is only in a terminal with a repl" when provided with facts that the JVM also provides real time observability.
> a "Proper GUI tool" is not.
I don't know what this means.
> hence why you need a full blown GUIS to manage the JVM,
Did you read the post you linked? The author required a GUI written by someone else.
I don't know what point you're trying to make anymore, you're just spitting out random sentences, that don't have to do with anything I was replying to (real time obserability).
You can write a library/tool that does exactly that, and the JVM offers an API precisely for that purpose. The reason such a tool isn't offered out of the box is that it trades off performance. For example, the JIT can optimise a variable or even an entire object away. When you want to inspect them at runtime, this would trigger a "deoptimization", where the representation would go back to the original bytecode from the optimised machine code. Observability that doesn't trigger deoptimization is, therefore, encouraged, but everything is available.
I don't know what that means. The JVM itself is at least as reliable as BEAM, and probably more so -- it's subject to both more development testing and field testing -- but what needs high reliability is applications. The JVM runs much more critical code than BEAM does, and on a far wider scale and more stressful workloads. The VM itself is the cause of neither Java's reliability nor Erlang's. It's the programming model that matters. The JVM can now support the Erlang model at least as well -- and, I claim, significantly better -- than BEAM ever could with its limited resources. BEAM's Achilles heel has always been performance. That has harmed Erlang application's reliability, as they need to depend on unsafe native code -- that can crash the entire process -- much more than JVM applications.
> The VM itself is the cause of neither Java's reliability nor Erlang's. It's the programming model that matters. The JVM can now support the Erlang model
That's a common misconception. There is a huge practical difference between "being able to write share-nothing" and "having whole ecosystem built around it". While you're referring to Erlang running unsafe native code, you're completely ignoring the Java running unsafe Java code. To ensure guarantees like "share nothing" or "restartable processes" Erlang developer just needs to write only Erlang and Java developer needs to carefully pick every dependency and almost always rewrite some of them.
That's the exact reason why Akka didn't replace OTP: you're most likely using the dependency which is not written with Akka, and it might share mutable memory, use global mutable state etc. So, even with possibility of writing Erlang-like software, you most certainly will end up with Frankenstein where some parts are fault tolerant, immutable, functional and reasonable, and some parts are oldschool java 8 bullshit.
Do you know what is the definition of DSL? It is when you write your code in one language and get exception in another. And that's exactly why almost every JVM language (except Java) is a DSL.
You just can't have guarantees Erlang has without rewriting everything with your Akka of the day.
I think you misunderstood my point. I wasn't trying to argue for Java over Erlang. The people writing Java will continue writing Java, and people writing Erlang will continue doing that.
I was merely pointing out that at this point, the JVM offers a great platform for Erlang itself, which could both increase its visibility and give it a technological boost. You could have everything be Erlang, top to bottom, with far better performance than BEAM could ever achieve, less work, and better interop with where a lot of people actually are.
There could be some disadvantages but overall I think it would be a great opportunity, and one that Erlang could really use.
> And that's exactly why almost every JVM language (except Java) is a DSL.
Other JVM languages choose to have "exceptions in Java" because it's so much less work for them to use existing libraries and they consider it a small enough price to pay. But even if you want virtually all of it to be Erlang, it would still be less work than BEAM.
You could even control exceptions and stack traces, as that's programmable:
Great, so you guys can spend the next 30 years writing OTP to work seamlessly and flawlessly on top of the new JVM with the same (memory, security, reliability, fault tolerance, etc) guarantees OTP has on the BEAM VM today :)
As the other reply noted, I’d be shocked if it wouldn’t be much better now, seeing something like graal being used would be really interesting. I think if Elixir could target beam or jvm it would be an amazing language for many tasks.
No, it wouldn't. Elixir is getting really fast computation through, e.g. nx, and the user story is incredible (OS install to stable diffusion in 40 minutes, most of which is dicking around figuring out how to install CUDA). Is it easy to run stable diffusion on jvm?
I think you might be taking my comment as implying the jvm is better than beam but that isn’t the argument I’m making. Having a strong jvm option means you can cut through a ton of corporate red tape. I don’t need to convince some skeptical CTO if the jvm is reasonable. It’s makes choosing elixir for a project feel about as hard a change as using clojure or groovy.
I don’t think we’re going to reach an agreement. I believe languages running on the jvm helps give assurance to businesses. You disagree with that belief, we can leave it there.
It likely would. But efficiency is only one factor. Many Erlang applications are far more concerned with consistent latency than throughput efficiency. So a switch to the JVM is a lot of cost.
You can take a look to the interview with Francesco Cesarini https://www.youtube.com/watch?v=-m31ag9z4VY for more details - here is provided a part where compared JVM with a BEAM.
Sure on a single machine, perhaps. but once you have multiple machines, the JVM would have to do what the BEAM does today; copy messages between processes regardless of location. That's going to slow down throughput.
Erlang has a highly optimized concurrent GC as well. It's just optimized for different things. And maybe the concurrency of the GC is different; Erlang has one heap per process (aka green thread), and no concurrency within a heap.
Erlang GC is also very simple and easy to understand because language features only allow references in one direction. Much of JVM GC complexity would be wasted as there's no need to look for reference loops and such, since they're not possible.
The JVM has a selection of four generational GCs. Two of them are the most sophisticated GCs available anywhere.
It's not a matter of some unique brilliance. Java is simply in such high demand that it has the sufficient resources to have state of the art optimising compilers and GCs.
This is a good point, thanks! I will extend the topic or maybe will be better to provide new topic as continuation of the current topic - since putting everything in one article can be difficult to understand and will increase the article itself, making it more difficult to read.
More copying if you pass values between processes. Honestly it would be really cool if you could mark off certain values that you know you're going to pass around and put them in a heap like the global binary heap.
there are lots of foot guns for the user with this model. because transferring data between processes involves copying this can become a problem. Erlang tries to optimise the handling of large binaries by using a separate reference counted heap. however, this introduces another set of issues where memory is 'leaked' because a smaller binary is holding a reference to a larger binary or because processes that have not been GC'd have not decremented the ref count of large binaries in the heap that they no longer user.
Scaling up an MQTT<->webhook relay that I wrote in Elixir to 1000’s of long running connections, I found that I needed to manually trigger periodic GCs on my long lived processes.
As binary strings work their way through the pipelines via messages, it leaves binaries on the binary heap that don’t go away because the ref count stays above 1. There are a number of GC parameters one can tune on a per process level that might cause a long lived process to collect more aggressively. But my long lived processes have a natural “ratchet” point where it was just easy to throw a collect in. This solved all of my slow growth memory problems.
I’ve read elsewhere that Erlangs GC benefits often on the basis that must Erlanger processes are short lived.
There was some work to try to make this use case work with normal GC (RefC binaries count as their size garbage, rather than just the size the reference is on the process heap). But if you know your process should be pretty clean at some point, manually triggering GC will do a better job. Off heap message passing might help this case too.
I’m not a big fan of framework mashups, so I’ve kept it pretty light and straightforward. Also, I was learning (and of course still am) when I started putting this together, so less things to learn was a boon.
- tortoise311 - I’ve toyed rewriting my own. We do very simple MQTT, 0 QoS, no wills, etc. the existing implementation creates many long lived procs per connection and we keep our connections live; they’re mostly subscribers
- bandit/plug - originally I was doing Phoenix because That’s The Thing, but it was such “A Way”, I was constantly having to learn how to accommodate things I just ended up turning off or suppressing. I just have straightforward (imo) API endpoints; Mat Trudel suggested I might just use Bandit with Plug. He’s done a great job with Bandit and been very proactive; just doing Plug myself helped me understand the whole HTTP handling pipeline at a more fundamental level
- CacheX - we use credentials oauth workflow. We were able to implement that in a single plug and use cachex. I may throw that out eventually. I’ve heard people indicate cachex has hung on them and it’s easy enough to do your own here
- Mint - I tried Finch and a couple other “help you” request frameworks. I had all kinds of problems tuning them as I moved up to many thousands of steady stream (every 10s+) hooks being dispatched. Eventually, I saw a comment in one of them that said something like “at any scale, you end up doing your own layer on top of mint to best fit the nuances of your application”, so I did just that, using the source from peppermint and finch to guide/inspire me
- openapispex -to swaggerify our endpoints; this requires a lot of boilerplate code and forced me to learn to write some of my own macros just to reduce it a little; I understand you get some of that for free when using it with Phoenix; the authors have been really helpful
- recon - because
There’s probably some stuff I should use that I’m not. But I’ve got a limited amount of time to improve this and keep native apps on two platforms running.
If I blogged, it’d be a good write up (how to do a kind of web thing — but without pages — without Phoenix!) maybe.
With CacheX being involved the memory leak you mention in your parent comment might be caused by ETS holding references to your binaries. I ran into something similar a few years ago and wrote about it here[0], but the tldr is that you can use `:binary.copy/1` or use the `:copy` option in the `Jason` module if you are using it.
ORCA (as part of the Pony compiled language) includes a more performant GC than C4 or BEAM/HiPE. It does so by reducing almost to zero the need to do global GC pauses by sharding the heap per actor, zero-copy message passing, fine-grained concurrent sharing semantics, and lock-free data structures.
I mean, the BEAM doesn't have global GC pauses either, as each process has its own heap - but I would expect Pony can take things a step further as a result of its strong type system, which IIRC is why it can support zero-copy messaging.
This is true. Erlang's heap per PID. Azul's C4 and other JVM GC move in the no world stopping direction but they're still at the mercy of the model of the JVM.
If one can avoid GCs altogether a-la precise (de)allocations like Rust's non-reference-counted entities, this is cool but often requires unnatural contortionism. RC is still necessary in certain cases.
For those problems that are amenable to Erlang's model, this is a fine solution. The only real improvement here would be making collection incremental.