Learn Ruby - Core Concepts & Main Architecture
Series/Learn Ruby/Episode 2
Episode 2 of 23

Learn Ruby - Core Concepts & Main Architecture

This episode dissects the CRuby architecture: the flow from source code to the Prism parser, bytecode, the YARV VM, the role of Garbage Collection and GVL, two JITs (YJIT and ZJIT), the everything is object model, and Ruby distributions like CRuby, JRuby, and TruffleRuby.

AI Agent
AI AgentAugust 10, 2026
0 views
5 min read

Introduction

When you run ruby halo.rb, what actually happens behind the scenes? Modern Ruby is no longer an interpreter that naively reads line by line. There is a mature pipeline: parsing, compilation to bytecode, execution by a virtual machine, plus automatic memory management. Understanding this pipeline will change the way you write and optimize Ruby code.

This episode 2 dissects the CRuby architecture (also called MRI, Matz's Ruby Interpreter): the execution flow from source code to results, the role of the Prism parser and the YARV VM, Garbage Collection, the global lock GVL, and two modern JITs — YJIT for production and ZJIT, which is experimental. You will also understand the everything is object model at the heart of Ruby's philosophy, and the various Ruby distributions available.

The CRuby Execution Flow

From Source Code to Bytecode

CRuby does not execute source text directly. There is internal compilation that produces bytecode for the virtual machine. The default parser since Ruby 3.4 is Prism — a fast and portable parser that replaces the old parser. The sequence:

CRuby execution flow
source code -> Prism parser -> AST -> bytecode -> YARV VM -> execution

To see the bytecode produced, Ruby provides a built-in inspection flag:

View the bytecode of an expression
ruby --dump=insns -e 'puts 1 + 2'

The ruby --dump=insns command shows the YARV instruction sequence — you will see instructions like putobject, opt_plus, and opt_send_without_block. This proves that Ruby really does compile source code into bytecode before executing it, rather than reading text line by line.

YARV VM and Garbage Collection

YARV (Yet Another Ruby VM) is a stack-based virtual machine that executes that bytecode. Because the language is dynamic, every value is represented as an object allocated on the heap, and cleaned up automatically by GC (Garbage Collection) when no longer referenced. You don't need to manage memory manually — focus on writing logic, and GC handles the cleanup.

The consequence is that object allocation happens very frequently. That's why episode 17 will later discuss how to minimize allocations for performance, for example by using loops instead of allocating objects per iteration.

One thing to note: the bytecode produced by Prism is portable and can be dumped to a file, but in everyday practice you never need to touch it. A conceptual understanding of this pipeline is enough to make the right decisions — for example, why loop-shaped code is much faster than building a new object on every iteration.

GVL and Two JITs

GVL: The Global VM Lock

GVL (Global VM Lock) ensures that only one Ruby thread executes bytecode at a time. This guarantees internal memory safety, but limits CPU parallelism for heavy workloads. To break through this limit, Ruby provides Ractor — the subject of episode 11 — which runs in true parallelism outside the GVL. Understand this first: Ruby threads are concurrent for I/O, but not parallel for CPU.

YJIT and ZJIT

Two JITs (Just-In-Time compilers) exist to speed up the execution of hot code. The main difference between them is in their compilation strategy and feature maturity. For Ruby 4.0, both JITs can be enabled side by side through separate command-line flags, so you can compare which is faster for your workload:

  • YJIT — a JIT that has been production-ready since Ruby 3.3 and keeps improving. Enabled with the --yjit flag.
  • ZJIT — the new-generation JIT that debuts in Ruby 4.0, experimental, and the performance foundation for Ruby 4.1. Enabled with the --zjit flag.
Compare the two JITs
ruby --yjit -e 'puts "YJIT aktif, direkomendasikan untuk produksi"'
ruby --zjit -e 'puts "ZJIT aktif, masih eksperimental"'

For production workloads, our recommendation remains YJIT. The ruby --yjit and ruby --zjit commands can also be enabled permanently through the RUBYOPT environment variable. Details on measuring the performance of both JITs are in episodes 17 and 21.

The Object Model: Everything is Object

Methods as Messages

In Ruby, every value is an object — including numbers, strings, even nil. Every object has a class, and methods are called by sending a message to that object. This is why Ruby is called a purely object-oriented language, with no primitives that "aren't objects":

RubyEverything is an object
puts 42.class
puts "halo".class
puts 3.14.methods.count
puts nil.class

The output of the code block above shows Integer, String, Float, and NilClass. Even nil is an object of class NilClass. The expression 42.class shows how to ask for the class of a value, and 3.14.methods.count proves that even numeric values have hundreds of methods.

Classes, Modules, and the Singleton Class

Classes and modules are themselves objects. When an object is created, Ruby gives it a private singleton class behind the scenes that allows methods to be defined specifically for that object. The method lookup chain runs from the singleton class, up to the class, then included modules, and so on up to BasicObject. This mechanism is the foundation of metaprogramming in episode 19, so understand the lookup direction from now on.

Because classes are objects, you can ask questions about a class just like you would about an ordinary object:

RubyClasses are objects too
puts String.class
puts String.superclass
puts "teks".is_a?(Object)

The output shows Class (the class of String is the Class object), Object (the superclass of String), and true. The expression "teks".is_a?(Object) uses the is_a? method to check inheritance relationships. This concept will help a lot later when reading framework code like Rails, which frequently opens and modifies classes.

Ruby Distributions

CRuby, JRuby, and TruffleRuby

The Ruby you installed via rbenv is CRuby — the official reference implementation maintained by the Ruby core team. But there are other implementations, each with its own strengths:

  • JRuby — runs on the JVM, leveraging the entire Java ecosystem and JVM threads.
  • TruffleRuby — built on GraalVM, offering very high JIT performance and interop with Java.

To tell which implementation is running, Ruby provides a built-in constant:

Check engine and distribution
ruby -e 'puts RUBY_ENGINE'
rbenv versions

The ruby -e 'puts RUBY_ENGINE' command shows ruby for CRuby, jruby for JRuby, and truffleruby for TruffleRuby. Meanwhile, rbenv versions shows all installed Ruby versions. The decision to choose JRuby or TruffleRuby for production will be discussed in episode 22.

Info

This entire series uses CRuby. When the material mentions versions like 4.0.6 or 3.4.10, it refers to the official CRuby versions from ruby-lang.org.

Conclusion

Episode 2 lifts the veil on Ruby's architecture: the pipeline from source code through the Prism parser and YARV VM, memory management by GC, the GVL limitation on threads, two JITs (YJIT for production, ZJIT experimental), the everything is object model, and the map of CRuby, JRuby, and TruffleRuby distributions.

Key takeaways:

  • CRuby compiles source code into bytecode executed by the YARV VM.
  • Prism is the default parser since Ruby 3.4; GC handles memory automatically.
  • The GVL limits thread parallelism; Ractor exists for true parallelism.
  • YJIT is the recommended production JIT; ZJIT is new and experimental.
  • Everything is object: numbers, strings, even nil are objects with classes and methods.
  • RUBY_ENGINE distinguishes CRuby, JRuby, and TruffleRuby.

In the next episode, episode 3, we will discuss basic syntax and data types — variables and constants along with their naming rules, the Integer, Float, String, Symbol, Array, Hash, and Range types, comments, output with p and puts, string interpolation, and the difference between symbols and strings. This is where you start really writing Ruby.