<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://headius.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://headius.github.io/" rel="alternate" type="text/html" /><updated>2026-07-13T20:44:05+00:00</updated><id>https://headius.github.io/feed.xml</id><title type="html">Charles Oliver Nutter</title><subtitle>Java, Ruby, and JVM guy trying to make sense of it all</subtitle><entry><title type="html">JRuby at RubyConf 2026</title><link href="https://headius.github.io/2026/07/jruby-at-rubyconf-2026.html" rel="alternate" type="text/html" title="JRuby at RubyConf 2026" /><published>2026-07-10T00:00:00+00:00</published><updated>2026-07-10T00:00:00+00:00</updated><id>https://headius.github.io/2026/07/jruby-at-rubyconf-2026</id><content type="html" xml:base="https://headius.github.io/2026/07/jruby-at-rubyconf-2026.html"><![CDATA[<p>This year marks a big milestone in my history working on JRuby: twenty years ago, <a href="https://blog.headius.com/2006/09/jruby-steps-into-sun.html">I joined Sun Microsystems</a> to work on the project full-time. This month also marks another milestone for JRuby: two years being independently funded by my company <a href="https://headius.com/welcome/">Headius Enterprises</a> through commercial support contracts. It’s a perfect time for us to return to <a href="https://rubyconf.org/">RubyConf 2026</a> and show today’s Rubyists what JRuby can do for them!</p>

<p><strong>Update!</strong> After a very long time, we have a new batch of JRuby t-shirts! Limited run so only folks at the JRuby talk, hack space, or cabana can get them! Also plenty of stickers for everyone!</p>

<h1 id="main-jruby-talk">Main JRuby Talk</h1>

<p>My goal with JRuby has always been to give Rubyists more tools and more opportunities. By combining JRuby with the power and ecosystem of the JVM, we’ve brought Ruby and Rails to places it never could have gone before. We’ve made it easy to integrate JVM libraries, deploy on existing Java infrastructure (including embedded and Android devices), and handle enormous applications that the standard runtime simply isn’t suited for.</p>

<p>This talk will provide a whirlwind 30-minute tour through JRuby’s history and design, showing several amazing use cases you might never imagine you could do with Ruby. Here’s the talk abstract:</p>

<blockquote>
  <p>JRuby is an implementation of Ruby 4.0 built on the Java Virtual Machine. For the past 20 years we’ve expanded the horizons of Ruby, bringing more jobs and more opportunities to Rubyists like you:</p>
  <ul>
    <li>High-scale applications needing shared-memory parallelism and big data</li>
    <li>Enterprise organizations with existing Java infrastructure * Cross-platform, commercial-off-the-shelf (COTS) application packaging that protects your code</li>
    <li>Graphics-intensive applications including desktop, games, and visualization</li>
    <li>Mobile and embedded solutions using Android And you can do it all in Ruby, with no Java experience required!
This talk will review JRuby’s history and show you what’s possible when you bring the best of Ruby and the JVM together.</li>
  </ul>
</blockquote>

<p>The main JRuby talk is scheduled for <a href="https://rubyconf.org/schedule/#:~:text=Expand%20your%20Ruby%20Potential%20with%20JRuby">Tuesday at 3:30PM</a>.</p>

<h1 id="jrubykaraoke-️">JRubyKaraoke! 🎤❤️</h1>

<p>On Tuesday Night, it’s time to sing! We are looking into options for a karaoke night with Ruby friends.</p>

<p>I’ve published a quick <a href="https://forms.gle/aXphxgUsnQYrpf3H8">JRuby at RubyConf 2026 form</a> to gauge interest. Please join us… it’s a Ruby tradition and I’d love to meet you and hang out!</p>

<h1 id="jruby-hack-space">JRuby Hack Space</h1>

<p>On Tuesday, the JRuby and Headius teams will manage a Hack Space table at the RubyConf 2026 Community Day!</p>

<p>Got more questions about JRuby? Have an application that needs the scaling, ecosystem, and performance that JRuby provides?Think you might like to contribute to the JRuby project?</p>

<p>We’ll be hanging out to answer your questions and help onboard you to the JRuby world. This is my favorite part of conferences: meeting people and helping them solve technical challenges!</p>

<p>Use the <a href="https://forms.gle/aXphxgUsnQYrpf3H8">JRuby at RubyConf 2026 form</a> to provide any information you think might help us support you.</p>

<h1 id="jruby-cabana">JRuby Cabana</h1>

<p>On Thursday, the Headius team would like to host you at our <strong>poolside cabana</strong> at the Red Rock Casino Resort, host of RubyConf 2026! We’ll be hanging out in the shade… talking, hacking, and relaxing between sessions.</p>

<p>Let us know on the <a href="https://forms.gle/aXphxgUsnQYrpf3H8">JRuby at RubyConf 2026 form</a> if you’re interested in dropping by!</p>

<h2 id="join-the-discussion-on-rruby">Join the discussion on r/ruby!</h2>

<blockquote class="reddit-embed-bq" style="height:500px" data-embed-height="240"><a href="https://www.reddit.com/r/ruby/comments/1ussn9g/jruby_at_rubyconf_2026_talk_hack_space_and_other/">JRuby at RubyConf 2026</a><br /> by<a href="https://www.reddit.com/user/headius/">u/headius</a> in<a href="https://www.reddit.com/r/ruby/">ruby</a></blockquote>
<script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>

<h1 id="keep-the-hits-coming"><em>Keep the Hits Coming</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>My blog posts, conference talks, and open-source work are all sponsored by <a href="https://headius.com">Headius Enterprises</a>, offering software support and development services for JVM and JRuby users. Our experts have decades of experience building and optimizing complex JVM applications and managing open-source projects.</p>

<p>If you are interested in <strong>scaling</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of the JVM’s <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert support from Headius Enterprises!</a></p>

<p>If you find my content or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the posts keep coming and the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[This year marks a big milestone in my history working on JRuby: twenty years ago, I joined Sun Microsystems to work on the project full-time. This month also marks another milestone for JRuby: two years being independently funded by my company Headius Enterprises through commercial support contracts. It’s a perfect time for us to return to RubyConf 2026 and show today’s Rubyists what JRuby can do for them!]]></summary></entry><entry><title type="html">Non-null variable declaration in Java using instanceof patterns</title><link href="https://headius.github.io/2025/12/inline-null-check-with-instanceof.html" rel="alternate" type="text/html" title="Non-null variable declaration in Java using instanceof patterns" /><published>2025-12-04T00:00:00+00:00</published><updated>2025-12-04T00:00:00+00:00</updated><id>https://headius.github.io/2025/12/inline-null-check-with-instanceof</id><content type="html" xml:base="https://headius.github.io/2025/12/inline-null-check-with-instanceof.html"><![CDATA[<p>Ever since <a href="https://blog.jruby.org/2025/04/jruby-10-part-1-whats-new">JRuby 10 upgraded to Java 21</a>, I’ve been re-learning Java with all the excellent language enhancements of the past decade. One of my favorites has to be the <code class="language-plaintext highlighter-rouge">instanceof</code> <a href="https://openjdk.org/jeps/394">pattern matching features</a> added in Java 16. Today I also realized I can use an <code class="language-plaintext highlighter-rouge">instanceof</code> pattern to null-check and assign a variable at the same time.</p>

<h1 id="using-instanceof-to-null-check">Using instanceof to null-check</h1>

<p>When checking if a value in Java is <code class="language-plaintext highlighter-rouge">instanceof</code> some type, we can get a false result in two cases:</p>

<ul>
  <li>The value is an instance of a type not equal to or descended from the specified type.</li>
  <li>The value is <code class="language-plaintext highlighter-rouge">null</code>.</li>
</ul>

<p>This second property turns instanceof into a weird sort of null-check when applied to a variable or method that matches the <code class="language-plaintext highlighter-rouge">instanceof</code> type, since we know the only false result must be when it’s <code class="language-plaintext highlighter-rouge">null</code>.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">foo</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">foo</span> <span class="k">instanceof</span> <span class="nc">String</span><span class="o">)</span> <span class="c1">// prints "false";</span>
</code></pre></div></div>

<p>Of course this is longer than just saying <code class="language-plaintext highlighter-rouge">foo == null</code> but <em>so what</em>? <strong>It works!</strong></p>

<h1 id="using-instanceof-patterns-for-inline-null-checking">Using instanceof patterns for inline null checking</h1>

<p>The above example doesn’t really have any practical use, which is probably why you never see folks talking about it. But when combined with an <code class="language-plaintext highlighter-rouge">instanceof</code> pattern, we can do something more fun.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// getString() is a method that might return null</span>
<span class="k">if</span> <span class="o">(</span><span class="n">firstCondition</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// something</span>
<span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">getString</span><span class="o">()</span> <span class="k">instanceof</span> <span class="nc">String</span> <span class="n">string</span><span class="o">)</span> <span class="o">{</span>
    <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"length: "</span> <span class="o">+</span> <span class="n">string</span><span class="o">.</span><span class="na">length</span><span class="o">());</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
    <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"string is null"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>We’ve just done an inline null check and variable declaration in the middle of an <code class="language-plaintext highlighter-rouge">if ... else</code> chain! The <code class="language-plaintext highlighter-rouge">string</code> value <strong>must</strong> be non-null in the <code class="language-plaintext highlighter-rouge">length</code> branch, so we can safely call methods on it. And we know it <strong>must</strong> be null if we end up in the final <code class="language-plaintext highlighter-rouge">else</code>.</p>

<h1 id="but-why">But why?</h1>

<p>Because it’s there?</p>

<p>Without <code class="language-plaintext highlighter-rouge">instanceof</code> patterns, you’d need to move everything after the <code class="language-plaintext highlighter-rouge">firstCondition</code> branch into the following <code class="language-plaintext highlighter-rouge">else</code>, declare a temporary variable for the value of <code class="language-plaintext highlighter-rouge">getString()</code>, and then do a nested null-checking branch. This pattern (pun intended) allows us to skip all that.</p>

<h3 id="is-it-better">Is it better?</h3>

<p>I can’t say. It’s frequently shorter, and requires less indented code.</p>

<h3 id="does-it-convey-the-intent-to-null-check">Does it convey the intent to null-check?</h3>

<p>Not if you don’t know about this “hidden” behavior of <code class="language-plaintext highlighter-rouge">instanceof</code>. That’s your fault, though!</p>

<h3 id="wont-it-be-slower-than-a-null-equality-check">Won’t it be slower than a null equality check?</h3>

<p>Actually, I had this question too, so I fired up the HotSpot disassembler.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Blah</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">foo</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s">"foo"</span><span class="o">);</span>
    <span class="kt">void</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">100000000</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">bar</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="kt">void</span> <span class="nf">bar</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">foo</span> <span class="k">instanceof</span> <span class="nc">String</span> <span class="n">string</span><span class="o">)</span> <span class="o">{</span>
            <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">string</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Blah.java
OpenJDK 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
...
</code></pre></div></div>

<p>It turns out, HotSpot’s just as smart as I am!</p>

<p>Here’s what the field access and <code class="language-plaintext highlighter-rouge">instanceof</code> turn into for this case:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  0x0000000117153544:   ldr		w11, [x1, #0xc]
  0x0000000117153548:   lsl		x10, x11, #3
                          ;*getfield foo {reexecute=0 rethrow=0 return_oop=0}
                          ; - Blah::bar@1 (line 9)
  0x000000011715354c:   cbnz	x10, #0x117153568
  0x0000000117153550:   ldp		x29, x30, [sp, #0x20]
  0x0000000117153554:   add		sp, sp, #0x30
  0x0000000117153558:   ldr		x8, [x28, #0x28]
                          ;   {poll_return}
  0x000000011715355c:   cmp		sp, x8
  0x0000000117153560:   b.hi	#0x117153580
  0x0000000117153564:   ret		
</code></pre></div></div>

<p>Instead of doing a more complex inheritance check, the <code class="language-plaintext highlighter-rouge">foo</code> field (loaded by <code class="language-plaintext highlighter-rouge">lsl</code> into the <code class="language-plaintext highlighter-rouge">x10</code> register from the <code class="language-plaintext highlighter-rouge">Blah</code> class in <code class="language-plaintext highlighter-rouge">x11</code>) is compared with zero (if that’s not the case, <code class="language-plaintext highlighter-rouge">cbnz</code> branches to the <code class="language-plaintext highlighter-rouge">println</code> branch at <code class="language-plaintext highlighter-rouge">0x117153568</code>), and then the rest of the instructions just tidy up and return from the <code class="language-plaintext highlighter-rouge">bar()</code> method.</p>

<p>Basically, because the only possible branch in the code is a null check, the JVM’s JIT will compile it as such.</p>

<p>So with the performance question out of the way, only one remains:</p>

<p>What do you think?</p>

<h2 id="join-the-discussion-on-rjava">Join the discussion on r/java!</h2>

<blockquote class="reddit-embed-bq" style="height:500px" data-embed-height="240"><a href="https://www.reddit.com/r/java/comments/1pdv016/nullchecking_the_fun_way_with_instanceof_patterns/">Null-checking the fun way with instanceof patterns</a><br /> by<a href="https://www.reddit.com/user/headius/">u/headius</a> in<a href="https://www.reddit.com/r/java/">java</a></blockquote>
<script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>

<h1 id="keep-the-hits-coming"><em>Keep the Hits Coming</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>My blog posts, conference talks, and open-source work are all sponsored by <a href="https://headius.com">Headius Enterprises</a>, offering software support and development services for JVM and JRuby users. Our experts have decades of experience building and optimizing complex JVM applications and managing open-source projects.</p>

<p>If you are interested in <strong>scaling</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of the JVM’s <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert support from Headius Enterprises!</a></p>

<p>If you find my content or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the posts keep coming and the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[Ever since JRuby 10 upgraded to Java 21, I’ve been re-learning Java with all the excellent language enhancements of the past decade. One of my favorites has to be the instanceof pattern matching features added in Java 16. Today I also realized I can use an instanceof pattern to null-check and assign a variable at the same time.]]></summary></entry><entry><title type="html">Warbled Sidekiq: Zero-install Executable for JVM</title><link href="https://headius.github.io/2025/10/warbled-sidekiq.html" rel="alternate" type="text/html" title="Warbled Sidekiq: Zero-install Executable for JVM" /><published>2025-10-23T00:00:00+00:00</published><updated>2025-10-23T00:00:00+00:00</updated><id>https://headius.github.io/2025/10/warbled-sidekiq</id><content type="html" xml:base="https://headius.github.io/2025/10/warbled-sidekiq.html"><![CDATA[<p>In my previous post, I showed <a href="https://blog.headius.com/2025/10/packaging-ruby-apps-with-warbler-jar-files.html">how to use Warbler</a> to package a simple image-processing tool as an executable jar. This post will demonstrate how to “warble” a larger project: the <a href="https://sidekiq.org/">Sidekiq background job server</a>!</p>

<h1 id="warbling-sidekiq">Warbling Sidekiq</h1>

<p>Sidekiq is one of the most successful packaged software projects in the Ruby world. It provides high-scale background job processing for Ruby applications atop Redis, and it has been commercially successful via enterprise features and support arrangements. It also happens to work great with JRuby and takes advantage of our excellent parallel threading capabilities.</p>

<p>Seems like a perfect use case for Warbler!</p>

<h2 id="the-easy-way">The Easy Way</h2>

<p>The easiest way to set up a new warbler configuration for an existing gem (that you may or may not control) is to create your own wrapper project.</p>

<p>I’ve started that for Sidekiq here: <a href="https://github.com/headius/sidekiq-warbler">sidekiq-warbler</a>.</p>

<p>You’ll notice the project is quite slim, containing only a few files for Warbler to package a Sidekiq executable JAR:</p>

<ul>
  <li>Gemfile and gemspec to specify dependencies. Note that this is not actually pushed as a gem, but Warbler currently auto-detects dependencies using these files.</li>
  <li>config/warble.rb to specify the <code class="language-plaintext highlighter-rouge">sidekiq</code> executable as the “main” and “sidekiq” as the filename for the JAR. I’ve removed unused configuration options from the generated warble.rb.</li>
  <li>Rakefile to load in rake tasks for warbler.</li>
  <li>README.md with basic usage information.</li>
</ul>

<p>Given this, the process is simple:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">git clone https://github.com/headius/sidekiq-warbler.git</code></li>
  <li><code class="language-plaintext highlighter-rouge">cd sidekiq-warbler</code></li>
  <li><code class="language-plaintext highlighter-rouge">bundle</code></li>
  <li><code class="language-plaintext highlighter-rouge">bundle exec rake jar</code></li>
</ul>

<p>Let’s see it in action, using the “plain old Ruby” example from Sidekiq itself:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sidekiq-warbler $ bundle
Fetching gem metadata from https://rubygems.org/............
Resolving dependencies...
Bundle complete! 2 Gemfile dependencies, 15 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
sidekiq-warbler $ bundle exec rake jar
rm -f sidekiq.jar
Creating sidekiq.jar
sidekiq-warbler $ java -jar sidekiq.jar -r ../sidekiq/examples/por.rb


               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:    ',$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   &lt;| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|
      

INFO  2025-10-23T15:37:27.684Z pid=85096 tid=1ryk: Running in jruby 10.0.2.0 (3.4.2) 2025-08-07 cba6031bd0 OpenJDK 64-Bit Server VM 21.0.8+9-LTS on 21.0.8+9-LTS +indy +jit [arm64-darwin]
INFO  2025-10-23T15:37:27.684Z pid=85096 tid=1ryk: See LICENSE and the LGPL-3.0 for licensing details.
INFO  2025-10-23T15:37:27.684Z pid=85096 tid=1ryk: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
INFO  2025-10-23T15:37:27.686Z pid=85096 tid=1ryk: Sidekiq 8.0.8 connecting to Redis with options {size: 10, pool_name: "internal", url: nil}
INFO  2025-10-23T15:37:27.707Z pid=85096 tid=1ryk: Sidekiq 8.0.8 connecting to Redis with options {size: 5, pool_name: "default", url: nil}
INFO  2025-10-23T15:37:27.710Z pid=85096 tid=1ryk: Starting processing, hit Ctrl-C to stop
</code></pre></div></div>

<p>It’s that easy!</p>

<h2 id="adding-official-warbler-support-to-sidekiq">Adding Official Warbler Support to Sidekiq</h2>

<p>In cases where you own or control a given gem, you can also add support for Warbler directly to the project. I’ve done that on a branch of Sidekiq here: https://github.com/headius/sidekiq/tree/warbled</p>

<p>You’ll notice a few differences in the “official support” <code class="language-plaintext highlighter-rouge">config/warble.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Hack to disable the "war" trait in Warbler</span>
<span class="k">class</span> <span class="nc">Warbler::Traits::War</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">detect?</span> <span class="o">=</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Warbler::Traits::Rack</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">detect?</span> <span class="o">=</span> <span class="kp">false</span>
<span class="k">end</span>

<span class="c1"># Warbler web application assembly configuration file</span>
<span class="no">Warbler</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">executable</span> <span class="o">=</span> <span class="s2">"bin/sidekiq"</span>
<span class="k">end</span>
</code></pre></div></div>

<ul>
  <li>The “hack” here disables Warbler’s automatic detection of web applications, so that we can just produce a plain executable JAR file instead of a web application WAR file. I’ve filed an issue with Warbler to make this easier in the future: https://github.com/jruby/warbler/issues/587</li>
  <li>The JAR file name is detected from the gem name (“sidekiq”) and we’re using a “main” script from the same gem, so the config is a bit simpler.</li>
</ul>

<p>I’ve also tweaked Sidekiq’s <code class="language-plaintext highlighter-rouge">Gemfile</code> to pin Rails at 7.1 (we’re in the process of shipping 7.2 and 8+ support for JRuby) and disable some native CRuby extensions we don’t support (<code class="language-plaintext highlighter-rouge">debug</code>, <code class="language-plaintext highlighter-rouge">vernier</code>, <code class="language-plaintext highlighter-rouge">ruby-prof</code>).</p>

<p>The result is basically the same:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sidekiq $ bundle exec rake jar
rm -f sidekiq.jar
Creating sidekiq.jar
sidekiq $ java -jar sidekiq.jar 
INFO  2025-10-23T16:01:50.836Z pid=86190 tid=1xca: ==================================================================
INFO  2025-10-23T16:01:50.837Z pid=86190 tid=1xca:   Please point Sidekiq to a Rails application or a Ruby file  
INFO  2025-10-23T16:01:50.837Z pid=86190 tid=1xca:   to load your job classes with -r [DIR|FILE].
INFO  2025-10-23T16:01:50.837Z pid=86190 tid=1xca: ==================================================================
INFO  2025-10-23T16:01:50.837Z pid=86190 tid=1xca: sidekiq [options]
    -c, --concurrency INT            processor threads to use
    -e, --environment ENV            Application environment
    -g, --tag TAG                    Process tag for procline
    -q, --queue QUEUE[,WEIGHT]       Queues to process with optional weights
    -r, --require [PATH|DIR]         Location of Rails application with jobs or file to require
    -t, --timeout NUM                Shutdown timeout
    -v, --verbose                    Print more verbose output
    -C, --config PATH                path to YAML config file
    -V, --version                    Print version and exit
    -h, --help                       Show help
</code></pre></div></div>

<h2 id="ipv4-vs-ipv6">IPv4 vs IPv6</h2>

<p>If you’re on a platform that supports IPv6 you may notice that JRuby (the JDK, really) will try to use IPv6 addresses and connections by default. On my system, the Redis server only bound itself to IPv4, preventing Sidekiq from being able to make that connection.</p>

<p>The magic flag to force the JDK to use IPv4 is <code class="language-plaintext highlighter-rouge">-Djava.net.preferIPv4Stack=true</code>. You can pass that directly to the <code class="language-plaintext highlighter-rouge">java</code> command, or use environment variable <code class="language-plaintext highlighter-rouge">JDK_JAVA_OPTIONS</code> as I do here:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sidekiq $ export JDK_JAVA_OPTIONS="-Djava.net.preferIPv4Stack=true"
sidekiq $ java -jar sidekiq.jar -r ./examples/por.rb
NOTE: Picked up JDK_JAVA_OPTIONS: -Djava.net.preferIPv4Stack=true


               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:    ',$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   &lt;| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|
      
...
</code></pre></div></div>

<h1 id="why-warble">Why Warble?</h1>

<p>We’ve seen that it’s pretty easy to turn a larger app like Sidekiq into an executable JAR file, but what’s the real advantage here?</p>

<p>Some of that comes from running on JRuby, which you can get with or without Warbler (Sidekiq works well and is regularly tested on JRuby):</p>

<ul>
  <li>Better parallel scaling across cores, for both Sidekiq itself and any garbage produced along the way (JVM’s GCs are highly concurrent and super scalable).</li>
  <li>Better tooling based on JVM profiling and monitoring features.</li>
</ul>

<p>And the other features are specific to a “warbled” executable JAR file:</p>

<ul>
  <li>“Zero-install” other than needing a JDK on the host system.</li>
  <li>No native libraries, no build tools, no additional dependencies.</li>
  <li>Enterprise-friendly: they don’t have to know or care about Ruby to deploy your product.</li>
  <li>IP-safe: Warbler can precompile Ruby code into JRuby’s bytecode format, making it very difficult to steal.</li>
</ul>

<p>Basically, if you want to ship Ruby tools and applications for modern organizations, JRuby and Warbler are a great way to get there!</p>

<h1 id="your-turn">Your Turn</h1>

<p>Obviously there’s enormous potential for Ruby tools and applications to be packaged and shipped using Warbler. If you’re interested in making this happen for you project, here’s the steps to take:</p>

<ul>
  <li>Make sure it runs on JRuby! Pure-Ruby libraries should “just work”, but more complicated apps may require alternative gems.</li>
  <li>Decide what your command-line “main” should look like when run as an executable jar. This may simply be your existing bin script.</li>
  <li>Follow docs on the <a href="https://github.com/jruby/warbler">Warbler project</a> for configuring and warbling your app.</li>
  <li>Profit! Enterprises love simple executable tools!</li>
</ul>

<p>JRuby is clearly the future of Ruby in the enterprise, and Warbler is a huge part of that. I am always here to help you build and package Ruby applications with JRuby and Warbler. Follow the links below and let’s set up a chat or call to help you bring your apps to a wider world!</p>

<h2 id="join-the-discussion-on-rruby">Join the discussion on r/ruby!</h2>

<blockquote class="reddit-embed-bq" style="height:500px" data-embed-height="240"><a href="https://www.reddit.com/r/ruby/comments/1oe8mx6/warbled_sidekiq_zeroinstall_executable_for_jvm/">Warbled Sidekiq: Zero-install Executable for JVM</a><br /> by<a href="https://www.reddit.com/user/headius/">u/headius</a> in<a href="https://www.reddit.com/r/ruby/">ruby</a></blockquote>
<script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>

<h1 id="jruby-support-for-your-project"><em>JRuby Support for Your Project</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>JRuby development is made possible by our primary sponsor, <a href="https://headius.com">Headius Enterprises</a>, offering a range of professional development and support resources for your team. You can choose JRuby for your next project knowing that you’ve got the world’s best JRuby experts standing by to help.</p>

<p>If you are interested in <strong>scaling Ruby</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of JVM features like <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, you need to be using JRuby. Let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>

<p>If you find this article or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[In my previous post, I showed how to use Warbler to package a simple image-processing tool as an executable jar. This post will demonstrate how to “warble” a larger project: the Sidekiq background job server!]]></summary></entry><entry><title type="html">Packaging Ruby Apps with Warbler: Executable JAR Files</title><link href="https://headius.github.io/2025/10/packaging-ruby-apps-with-warbler-jar-files.html" rel="alternate" type="text/html" title="Packaging Ruby Apps with Warbler: Executable JAR Files" /><published>2025-10-21T00:00:00+00:00</published><updated>2025-10-21T00:00:00+00:00</updated><id>https://headius.github.io/2025/10/packaging-ruby-apps-with-warbler-jar-files</id><content type="html" xml:base="https://headius.github.io/2025/10/packaging-ruby-apps-with-warbler-jar-files.html"><![CDATA[<p><a href="https://github.com/jruby/warbler">Warbler</a> is the JRuby ecosystem’s tool for packaging up Ruby apps with all dependencies in a single deployable file. We’ve just released an update, so let’s explore how to use Warbler to create all-in-one packaged Ruby apps!</p>

<h1 id="application-packaging-for-the-java-world">Application Packaging for the Java World</h1>

<p>The Java world has been creating distributable, “run anywhere” packages since Java was first released in 1996. Java source code is compiled to bytecode, stored in .class files and then archived together with metadata in JAR files (Java ARchive) that can be run as command-line executable files or as deployable web applications. A JAR file is just a zip file, laid out in a specific way to contain the code and resources your application or library needs, and essentially all libraries for the JVM get distributed as JAR files.</p>

<p>The JRuby distribution includes <code class="language-plaintext highlighter-rouge">lib/jruby.jar</code>, for example, which is where all of the internal JRuby Java and Ruby code is located:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby $ jruby -v
jruby 10.0.3.0-SNAPSHOT (3.4.5) 2025-10-10 0ea455e57a OpenJDK 64-Bit Server VM 21.0.8+9-LTS on 21.0.8+9-LTS +indy +jit [arm64-darwin]
jruby $ ls -l lib/jruby.jar
-rw-r--r--  1 headius  staff  16928929 Oct 14 12:59 lib/jruby.jar
jruby $ java -jar lib/jruby.jar -v
jruby 10.0.3.0-SNAPSHOT (3.4.5) 2025-10-10 0ea455e57a OpenJDK 64-Bit Server VM 21.0.8+9-LTS on 21.0.8+9-LTS +indy +jit [arm64-darwin]
</code></pre></div></div>

<p>When distributed as a deployable web application, the filename typically ends with “.war” (Web ARchive) and contains a combination of code, configuration, and other JAR files (dependencies of the app). Enterprise applications are packaged in “.ear” files (Enterprise ARchive) which will in turn contain jars and wars and additional configuration for enterprise Java servers. At the end of the day, though, they’re all just zip files laid out in a particular way.</p>

<h2 id="write-once-run-anywhere">Write Once, Run Anywhere</h2>

<p>This is all possible because of the Write Once, Run Anywhere (WORA) promise of JVM bytecode: if your application runs on the JVM, you can write, compile, and package it once and deploy it anywhere without recompiling.</p>

<p>Compare this to the way external dependencies are managed in Ruby:</p>

<ul>
  <li>Gem files provide installable packaging for Ruby libraries, but they must be unpacked into a specific path on the filesystem to be usable.</li>
  <li>Libraries with native code require separate binary gems for each platform or must be built at install time and installed on the filesystem.</li>
  <li>Ruby itself must be built for each platform you plan to run it on, or downloaded as a platform-specific binary.</li>
</ul>

<p>Wouldn’t it be nice if we could package Ruby applications for distribution without these hassles, in a way that enterprises and security-conscious companies can handle? That’s where Warbler comes in!</p>

<h2 id="warbler-packaging-tool-for-jruby-applications">Warbler: packaging tool for JRuby applications</h2>

<p>Warbler is the JRuby tool of choice for packaging Ruby applications for distribution. Given a Ruby utility or application, Warbler can:</p>

<ul>
  <li>Package your Ruby utility <strong>along with gem and jar dependencies</strong> (including JRuby itself) as a single executable JAR file.</li>
  <li>Package your Rails, Hanami, Sinatra or other web application as a <strong>deployable WAR file</strong> (again with all dependencies included).</li>
  <li>Add a <strong>mini-server</strong> to your WAR file so it can be <strong>run directly at the command line</strong>.</li>
  <li>Precompile all Ruby code to JRuby’s bytecode format, to <strong>obfuscate and protect your intellectual property</strong>.</li>
</ul>

<p>Let’s try a simple example to get started.</p>

<h1 id="warbler-in-practice">Warbler in Practice</h1>

<p>We’ll be packaging a demo utility called <a href="https://github.com/headius/image_voodoo_demo">image_voodoo_demo</a> for these examples, based on the <a href="https://github.com/jruby/image_voodoo">image_voodoo</a> image-manipulation gem.</p>

<p>ImageVoodoo is a Ruby wrapper around the JDK’s built-in image-processing APIs (inspired by the ImageMagick gem), allowing JRuby users to scale, crop, thumbnail, greyscale, and apply other transformations to images without installing external libraries. This demo takes the example script from ImageVoodoo and turns it into a command-line utility called <code class="language-plaintext highlighter-rouge">voodoo</code>.</p>

<p>Here’s basic use of the <code class="language-plaintext highlighter-rouge">voodoo</code> command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gem install image_voodoo_demo
Successfully installed image_voodoo_demo-0.1.0
Parsing documentation for image_voodoo_demo-0.1.0
Done installing documentation for image_voodoo_demo after 0 seconds
1 gem installed
$ voodoo PXL_20250802_162259596.jpg 
wrote demo files to /Users/headius/work/jruby/PXL_20250802_162259596/
$ ls -1 PXL_20250802_162259596 
border.jpg
cropped_thumb.jpg
cropped.jpg
reduced.jpg
resized.jpg
resized.png
thumb.jpg
</code></pre></div></div>

<p>If you’ve already got Ruby or JRuby installed, this workflow is fine. But what if you want to distribute this utility to someone with no knowledge of the Ruby ecosystem? Let’s package image_voodoo_demo as an executable JAR file!</p>

<h2 id="executable-jar-files">Executable JAR Files</h2>

<p>Java’s JAR files can be marked as “executable” by including metadata that configures a “main” entry point. In the case of JRuby, the “main” points to <code class="language-plaintext highlighter-rouge">org.jruby.main.Main</code>, which bootstraps JRuby and runs your code. Warbler can generate a JAR file for your Ruby tool that includes this configuration and launches your Ruby tool directly.</p>

<p>The simplest way to do this is to run Warbler from within the source repo for your utility.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ ls -1                         
exe
Gemfile
Gemfile.lock
image_voodoo_demo.gemspec
lib
Rakefile
README.md
</code></pre></div></div>

<p>image_voodoo_demo has a pretty standard gem layout, with a <code class="language-plaintext highlighter-rouge">gemspec</code> and <code class="language-plaintext highlighter-rouge">Gemfile</code> to specify dependencies and gem configuration.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ cat exe/voodoo 
#!/usr/bin/env ruby

require 'image_voodoo/demo'

filename = ARGV[0]

ImageVoodoo::Demo.output_demo_files(filename)

image_voodoo_demo $ tail image_voodoo_demo.gemspec 
  spec.bindir = "exe"
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]

  # Uncomment to register a new dependency of your gem
  spec.add_dependency "image_voodoo", "0.9.3"

  # For more information and examples about making a new gem, check out our
  # guide at: https://bundler.io/guides/creating_gem.html
end
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">voodoo</code> command just loads the demo code and executes it against <code class="language-plaintext highlighter-rouge">ARGV[0]</code>, and the <code class="language-plaintext highlighter-rouge">gemspec</code> includes <code class="language-plaintext highlighter-rouge">exe/voodoo</code> as its sole executable.</p>

<p>With this layout and configuration, using Warbler is simple!</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ gem install warbler
Fetching warbler-2.1.0.gem
Successfully installed warbler-2.1.0
Parsing documentation for warbler-2.1.0
Installing ri documentation for warbler-2.1.0
Done installing documentation for warbler after 0 seconds
1 gem installed
image_voodoo_demo $ warble
rm -f image_voodoo_demo.jar
Creating image_voodoo_demo.jar
</code></pre></div></div>

<p>We’ve just created an executable JAR containing everything needed for the <code class="language-plaintext highlighter-rouge">voodoo</code> command:</p>

<ul>
  <li>The code and executable for image_voodoo_demo</li>
  <li>The image_voodoo gem</li>
  <li>JRuby’s own JAR file and dependencies</li>
  <li>The Ruby standard library</li>
</ul>

<p>We run the jar with the <code class="language-plaintext highlighter-rouge">java -jar</code> command line:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ java -jar image_voodoo_demo.jar ~/Downloads/PXL_20250802_162259596.jpg 
wrote demo files to /Users/headius/work/image_voodoo_demo/PXL_20250802_162259596/
</code></pre></div></div>

<p>It works!</p>

<h2 id="going-deeper">Going Deeper</h2>

<p>Here’s an abbreviated listing of the contents of the <code class="language-plaintext highlighter-rouge">image_voodoo_demo.jar</code> file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ jar tf image_voodoo_demo.jar 
JarMain.class
META-INF/
META-INF/MANIFEST.MF
META-INF/init.rb
META-INF/lib/
META-INF/lib/jruby-core-10.0.2.0-complete.jar
META-INF/lib/jruby-stdlib-10.0.2.0.jar
META-INF/main.rb
gems/
gems/bundler-2.6.9/
gems/bundler-2.6.9/exe/
gems/bundler-2.6.9/exe/bundle
gems/bundler-2.6.9/exe/bundler
gems/image_voodoo-0.9.3/
gems/image_voodoo-0.9.3/Gemfile
gems/image_voodoo-0.9.3/History.txt
gems/image_voodoo-0.9.3/Jars.lock
...
image_voodoo_demo/
image_voodoo_demo/Gemfile
image_voodoo_demo/Gemfile.lock
image_voodoo_demo/README.md
image_voodoo_demo/Rakefile
image_voodoo_demo/exe/
image_voodoo_demo/exe/voodoo
image_voodoo_demo/lib/
image_voodoo_demo/lib/image_voodoo/
image_voodoo_demo/lib/image_voodoo/demo/
image_voodoo_demo/lib/image_voodoo/demo.rb
image_voodoo_demo/lib/image_voodoo/demo/version.rb
specifications/
specifications/bundler-2.6.9.gemspec
specifications/image_voodoo-0.9.3.gemspec
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">JarMain.class</code> is the “main” file for the JVM. It sets up the JRuby runtime and in-archive gem paths and then launches our executable <code class="language-plaintext highlighter-rouge">voodoo</code> command.</li>
  <li><code class="language-plaintext highlighter-rouge">META-INF</code> contains the JAR metadata along with any dependency libraries. In this case it just needs the <code class="language-plaintext highlighter-rouge">jruby-core</code> and <code class="language-plaintext highlighter-rouge">jruby-stdlib</code> jars to have a complete JRuby runtime available.</li>
  <li><code class="language-plaintext highlighter-rouge">gems</code> contains all dependency gems for this tool.</li>
  <li><code class="language-plaintext highlighter-rouge">image_voodoo_demo</code> naturally contains our demo tool’s source files.</li>
</ul>

<p>Let’s customize the building of this jar a little bit.</p>

<h2 id="customizing-warbler">Customizing Warbler</h2>

<p>Warbler supports a number of configuration settings and can dump a dummy config file to <code class="language-plaintext highlighter-rouge">config/warble.rb</code>:</p>

<p><em>(Note: the config directory must exist to generate the config file; this will be fixed in warbler 2.1.1)</em></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ warble config 
cp /Users/headius/work/jruby/lib/ruby/gems/shared/gems/warbler-2.1.0/warble.rb config/warble.rb
</code></pre></div></div>

<p>Let’s say my <code class="language-plaintext highlighter-rouge">image_voodoo_demo</code> utility is super-proprietary IP but I need to deliver it to a customer for use on-premises. The configuration setting we’re looking for is <code class="language-plaintext highlighter-rouge">features</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ head -12 config/warble.rb
# Disable Rake-environment-task framework detection by uncommenting/setting to false
# Warbler.framework_detection = false

# Warbler web application assembly configuration file
Warbler::Config.new do |config|
  # Features: additional options controlling how the jar is built.
  # Currently the following features are supported:
  # - *gemjar*: package the gem repository in a jar file in WEB-INF/lib
  # - *executable*: embed a web server and make the war executable
  # - *runnable*: allows to run bin scripts e.g. `java -jar my.war -S rake -T`
  # - *compiled*: compile .rb files to .class files
  # config.features = %w(gemjar)
</code></pre></div></div>

<p>We modify this line to <code class="language-plaintext highlighter-rouge">config.features = %w[compiled]</code> and re-run the <code class="language-plaintext highlighter-rouge">warble</code> command:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ warble             
java -classpath "/Users/headius/work/jruby/lib/ruby/gems/shared/gems/jruby-jars-10.0.2.0/lib/jruby-core-10.0.2.0-complete.jar":"/Users/headius/work/jruby/lib/ruby/gems/shared/gems/jruby-jars-10.0.2.0/lib/jruby-stdlib-10.0.2.0.jar"  \
        org.jruby.Main -S jrubyc  "lib/image_voodoo/demo.rb" "lib/image_voodoo/demo/version.rb"
Ignoring prism-1.5.2 because its extensions are not built. Try: gem pristine prism --version 1.5.2
Ignoring resolv-0.6.2 because its extensions are not built. Try: gem pristine resolv --version 0.6.2
rm -f image_voodoo_demo.jar
Creating image_voodoo_demo.jar
rm -f lib/image_voodoo/demo.class lib/image_voodoo/demo/version.class
</code></pre></div></div>

<p>Our build has changed a bit, using JRuby’s <code class="language-plaintext highlighter-rouge">jrubyc</code> command-line compiler to precompile the <code class="language-plaintext highlighter-rouge">image_voodoo_demo</code> sources into JVM class files containing JRuby bytecode.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ jar tf image_voodoo_demo.jar | grep image_voodoo_demo/lib
image_voodoo_demo/lib/
image_voodoo_demo/lib/image_voodoo/
image_voodoo_demo/lib/image_voodoo/demo/
image_voodoo_demo/lib/image_voodoo/demo.class
image_voodoo_demo/lib/image_voodoo/demo.rb
image_voodoo_demo/lib/image_voodoo/demo/version.class
image_voodoo_demo/lib/image_voodoo/demo/version.rb
</code></pre></div></div>

<p>The .rb files here are just stubs to load the .class files:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ unzip image_voodoo_demo.jar image_voodoo_demo/lib/image_voodoo/demo.rb
Archive:  image_voodoo_demo.jar
replace image_voodoo_demo/lib/image_voodoo/demo.rb? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
  inflating: image_voodoo_demo/lib/image_voodoo/demo.rb  
image_voodoo_demo $ cat image_voodoo_demo/lib/image_voodoo/demo.rb 
load __FILE__.sub(/.rb$/, '.class')%  
</code></pre></div></div>

<p>We can use the <code class="language-plaintext highlighter-rouge">javap</code> disassembler to see the contents of one of these precompiled class files. Good luck turning this back into source code!</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image_voodoo_demo $ javap -c -classpath image_voodoo_demo.jar image_voodoo_demo/lib/image_voodoo/demo
Warning: File image_voodoo_demo.jar(/image_voodoo_demo/lib/image_voodoo/demo.class) does not contain class image_voodoo_demo/lib/image_voodoo/demo
Compiled from "lib/image_voodoo/demo.rb"
public class lib.image_voodoo.demo {
  public static {};
    Code:
       0: new           #11                 // class java/lang/StringBuilder
       3: dup
       4: invokespecial #14                 // Method java/lang/StringBuilder."&lt;init&gt;":()V
       7: ldc           #16                 // String \u0000\u0000\u0000\u0002\u0000\u0000\n®\b\tH\u0002\"\u0001\u0000S\u0001z\fimage_voodooÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0002\u0000t\u0000\u0000H\u0003\"\u0001\u0000S\u0001z\u0019image_voodoo/demo/versionÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0003\u0000t\u0000\u0001H\u00053t\u0000\u0002\u0001_\u0000u-t\u0000\u0002\u0001\u0007requireÿÿÿÿÿ\u0007Xt\u0006\u0000_\u0000I\u0001t\u0006\u0000\u0000\u0018lib/image_voodoo/demo.rb\u0006H\u00062t\u0000\u0001_\u0000\u0002H\u0018I\u0002t\u0006\u0000\u0000\u0018lib/image_voodoo/demo.rb\u0019-t\u0000\u0001\u0001ÿÿÿÿÿ\u0007\tXt\u0006\u0000_\u0000I\u0001t\u0006\u0000\u0000\u0018lib/image_voodoo/demo.rb\u00076S\u0003H\u0017I\u0002t\u0006\u0000\u0000\u0018lib/image_voodoo/demo.rb\u0018-:\u0001\u0002ÿÿÿÿÿ\u0011output_demo_filesÿÿÿÿÿ\b\t\ft\u0000\u0000ffU\u0001\u0000fÿÿÿÿÿt\u0000\u0000\nl\u0000\u0000t\u0000\u0000\u0000H\b?t\u0000\u0001s\u0001f$\u0000\u0002t\u0000\u0001ÿÿÿÿþl\u0000\u0000wS\u0004\u0000t\u0000\u0002-t\u0000\u0002\u0003\bfilenameÿÿÿÿÿ\u000bImageVoodooÿÿÿÿÿ\nwith_imageÿÿÿÿÿ0;L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\t\ft\u0005\u0000\u0001ff\nl\u0000\u0000t\u0005\u0000\u0001\u0000H\t?t\u0005\u0001\u0001s\u0001f#\u0000\u0002t\u0005\u0001\u0001\u0002l\u0003\u0001z\u0002.*ÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\t\u0000t\u0005\u0002\u0001Xl\u0002\u0000t\u0005\u0002\u0001H\n?t\u0005\u0003\u0001s\u0004f\"\u0000\u0005t\u0005\u0003\u0001\u0001l\u0002\u0000\u0000t\u0005\u0004\u0001\u0006L\u0007CL1_LBL\u0000t\u0005\u0004\u0001Xt\u0005\u0005\u0001N\u0001L\u0007CL1_LBL\u0001:L\u0007CL1_LBL\u0000?t\u0005\u0006\u0001s\u0004f\"\u0000\u0006t\u0005\u0006\u0001\u0001l\u0002\u0000\u0000t\u0005\u0007\u0001Xt\u0005\u0005\u0001t\u0005\u0007\u0001:L\u0007CL1_LBL\u0001H\u000b$\u0000\u0007l\u0000\u0000ÿÿÿÿþfdwS\u0005\u0000t\u0005\b\u0001H\f\u0017\u0000\bl\u0000\u0000ÿÿÿÿûfdfÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0000Èfÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0090fÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0002XwS\u0006\u0000t\u0005\t\u0001H\r$\u0000\tl\u0000\u0000ÿÿÿÿþf2wS\u0007\u0000t\u0005\n\u0001H\u000e\u0017\u0000\nl\u0000\u0000ÿÿÿÿýfdfÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0096wS\b\u0000t\u0005\u000b\u0001H\u0012Xt\u0005\r\u0001{\u0002:\u000bf\u0014:\fz\u0006FF0000ÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0012\"\u0000\rl\u0000\u0000\u0001t\u0005\r\u0001\u0002t\u0005\f\u0001Pt\u0005\u000f\u0001\u0002l\u0002\u0000z\u000b/border.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0012ÿÿÿÿþ\u000fft\u0018lib/image_voodoo/demo.rb\u0012\"\u0000\u000et\u0005\f\u0001\u0001t\u0005\u000f\u0001\u0000t\u0005\u000e\u0001H\u0013!\u0000\u000fl\u0000\u0000\u0001F?è\u0000\u0000\u0000\u0000\u0000\u0000\u0000t\u0005\u0010\u0001Pt\u0005\u0012\u0001\u0002l\u0002\u0000z\f/reduced.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0013ÿÿÿÿþ\u0010ft\u0018lib/image_voodoo/demo.rb\u0013\"\u0000\u000et\u0005\u0010\u0001\u0001t\u0005\u0012\u0001\u0000t\u0005\u0011\u0001H\u0014?t\u0005\u0015\u0001s\u0004f%\u0000\u0010t\u0005\u0015\u0001\u0000\u0000t\u0005\u0016\u0001Pt\u0005\u0014\u0001\u0005z\u0014wrote demo files to ÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0014t\u0005\u0016\u0001z\u0001/ÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0014l\u0002\u0000z\u0001/ÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0014ÿÿÿÿþ\u001eft\u0018lib/image_voodoo/demo.rb\u0014\"\u0001\u0011S\u0001t\u0005\u0014\u0001\u0000t\u0005\u0013\u0001-t\u0005\u0013\u0001&lt;:L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\u0012t\u0005\u0017\u0001ct\u0005\u0018\u0001\u0002\u0001t\u0005\u0017\u00010t\u0005\u0018\u0001:L\u0007CL1_LBL\u0002\u0012\u0003imgÿÿÿÿÿ\u0004Fileÿÿÿÿÿ\bbasenameÿÿÿÿÿ\bfilenameÿÿÿÿÿ\u0003Dirÿÿÿÿÿ\u0006exist?ÿÿÿÿÿ\u0005mkdirÿÿÿÿÿ\u0011cropped_thumbnailÿÿÿÿÿ\twith_cropÿÿÿÿÿ\tthumbnailÿÿÿÿÿ\u0006resizeÿÿÿÿÿ\u0005widthÿÿÿÿÿ\u0005colorÿÿÿÿÿ\nadd_borderÿÿÿÿÿ\u0004saveÿÿÿÿÿ\u0007qualityÿÿÿÿÿ\u0003pwdÿÿÿÿÿ\u0004putsÿÿÿÿÿ\r;L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\ft\u0005\u0000\u0002ff\nl\u0000\u0000t\u0005\u0000\u0002\u0000H\u000bPt\u0005\u0002\u0002\u0002l\u0001\u0001z\u0012/cropped_thumb.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u000bÿÿÿÿþ\u0016ft\u0018lib/image_voodoo/demo.rb\u000b\"\u0000\u0002l\u0000\u0000\u0001t\u0005\u0002\u0002\u0000t\u0005\u0001\u0002-t\u0005\u0001\u0002&lt;:L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\u0012t\u0005\u0003\u0002ct\u0005\u0004\u0002\u0002\u0001t\u0005\u0003\u00020t\u0005\u0004\u0002:L\u0007CL2_LBL\u0000\u0003\u0004img2ÿÿÿÿÿ\bbasenameÿÿÿÿÿ\u0004saveÿÿÿÿÿ\r;L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\ft\u0005\u0000\u0003ff\nl\u0000\u0000t\u0005\u0000\u0003\u0000H\fPt\u0005\u0002\u0003\u0002l\u0001\u0001z\f/cropped.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\fÿÿÿÿþ\u0010ft\u0018lib/image_voodoo/demo.rb\f\"\u0000\u0002l\u0000\u0000\u0001t\u0005\u0002\u0003\u0000t\u0005\u0001\u0003-t\u0005\u0001\u0003&lt;:L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\u0012t\u0005\u0003\u0003ct\u0005\u0004\u0003\u0002\u0001t\u0005\u0003\u00030t\u0005\u0004\u0003:L\u0007CL3_LBL\u0000\u0003\u0004img2ÿÿÿÿÿ\bbasenameÿÿÿÿÿ\u0004saveÿÿÿÿÿ\r;L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\ft\u0005\u0000\u0004ff\nl\u0000\u0000t\u0005\u0000\u0004\u0000H\rPt\u0005\u0002\u0004\u0002l\u0001\u0001z\n/thumb.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\rÿÿÿÿþ\u000eft\u0018lib/image_voodoo/demo.rb\r\"\u0000\u0002l\u0000\u0000\u0001t\u0005\u0002\u0004\u0000t\u0005\u0001\u0004-t\u0005\u0001\u0004&lt;:L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\u0012t\u0005\u0003\u0004ct\u0005\u0004\u0004\u0002\u0001t\u0005\u0003\u00040t\u0005\u0004\u0004:L\u0007CL4_LBL\u0000\u0003\u0004img2ÿÿÿÿÿ\bbasenameÿÿÿÿÿ\u0004saveÿÿÿÿÿ\u0010;L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\ft\u0005\u0000\u0005ff\nl\u0000\u0000t\u0005\u0000\u0005\u0000H\u000fPt\u0005\u0002\u0005\u0002l\u0001\u0001z\f/resized.jpgÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u000fÿÿÿÿþ\u0010ft\u0018lib/image_voodoo/demo.rb\u000f\"\u0000\u0002l\u0000\u0000\u0001t\u0005\u0002\u0005\u0000t\u0005\u0001\u0005H\u0010Pt\u0005\u0004\u0005\u0002l\u0001\u0001z\f/resized.pngÿÿÿÿþ\u0010\u0018lib/image_voodoo/demo.rb\u0010ÿÿÿÿþ\u0010ft\u0018lib/image_voodoo/demo.rb\u0010\"\u0000\u0002l\u0000\u0000\u0001t\u0005\u0004\u0005\u0000t\u0005\u0003\u0005-t\u0005\u0003\u0005&lt;:L\u0015_GLOBAL_ENSURE_BLOCK_\u0000\u0012t\u0005\u0005\u0005ct\u0005\u0006\u0005\u0002\u0001t\u0005\u0005\u00050t\u0005\u0006\u0005:L\u0007CL5_LBL\u0000\u0003\u0004img2ÿÿÿÿÿ\bbasenameÿÿÿÿÿ\u0004saveÿÿÿÿÿ\t\u0007\u0000\u0003\u0000\u0000\u0000\u0000ÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000 \u0005ffffffffffff\u0000\bÿ\u0000\u0000\u0000\u0097\u0005\u0005\u0002\u0000\u000bImageVoodooÿÿÿÿÿ\u0000\u0000\u0000\u0000ÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000?øffffffffffff\u0000ÿ\u0000\u0000\u0000¥ÿ\u0000\u0000\u0000û\u0004\u0006\u0001\u0000\u0004Demoÿÿÿÿÿ\u0001\u0000\u0000\u0000ÿ\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000?øffffffffffff\u0000ÿ\u0000\u0000\u0001\u0001ÿ\u0000\u0000\u0001Q\u0003\u0007\u0003\u0000\u0011output_demo_filesÿÿÿÿÿ\u0002\u0000\u0001\bfilename\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000?ýffffffffffff\u0000ÿ\u0000\u0000\u0001nÿ\u0000\u0000\u0001¬\u0000\b\u0019\u0003fÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿ\u0012with_image &amp;|img|1ÿÿÿÿÿ\u0003\u0001\u0002\u0003img\bbasename\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000?ýffffftffffff\u0000ÿ\u0000\u0000\u0001Üÿ\u0000\u0000\u0005\u0082\u0000\u000b\u0005\u0001fÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿ\u001acropped_thumbnail &amp;|img2|2ÿÿÿÿÿ\u0004\u0001\u0001\u0004img2\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000 \u0000ffffftffffff\u0000ÿ\u0000\u0000\u0006cÿ\u0000\u0000\u0007=\u0000\f\u0005\u0001fÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿ\u0012with_crop &amp;|img2|3ÿÿÿÿÿ\u0004\u0001\u0001\u0004img2\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000 \u0000ffffftffffff\u0000ÿ\u0000\u0000\u0007`ÿ\u0000\u0000\b4\u0000\r\u0005\u0001fÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿ\u0012thumbnail &amp;|img2|4ÿÿÿÿÿ\u0004\u0001\u0001\u0004img2\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000 \u0000ffffftffffff\u0000ÿ\u0000\u0000\bWÿ\u0000\u0000\t)\u0000\u000e\u0007\u0001fÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿ\u000fresize &amp;|img2|5ÿÿÿÿÿ\u0004\u0001\u0001\u0004img2\u0000ÿ\u0000\u0001\u0000\u0000\u0000\u0000\u0000ÿÿ\u0000\u0000 \u0000ffffftffffff\u0000ÿ\u0000\u0000\tLÿ\u0000\u0000\n\u008b
       9: invokevirtual #20                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      12: invokevirtual #24                 // Method java/lang/Object.toString:()Ljava/lang/String;
      15: putstatic     #26                 // Field script_ir:Ljava/lang/String;
      18: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #34                 // Method org/jruby/Ruby.newInstance:()Lorg/jruby/Ruby;
       3: astore_1
       4: aload_1
       5: aload_1
       6: getstatic     #26                 // Field script_ir:Ljava/lang/String;
       9: ldc           #36                 // String ISO-8859-1
      11: invokevirtual #42                 // Method java/lang/String.getBytes:(Ljava/lang/String;)[B
      14: ldc           #43                 // String lib/image_voodoo/demo.rb
      16: invokestatic  #49                 // Method org/jruby/ir/runtime/IRRuntimeHelpers.decodeScopeFromBytes:(Lorg/jruby/Ruby;[BLjava/lang/String;)Lorg/jruby/ir/IRScope;
      19: invokevirtual #53                 // Method org/jruby/Ruby.runInterpreter:(Lorg/jruby/ParseResult;)Lorg/jruby/runtime/builtin/IRubyObject;
      22: return

  public static org.jruby.ir.IRScope loadIR(org.jruby.Ruby, java.lang.String);
    Code:
       0: aload_0
       1: getstatic     #26                 // Field script_ir:Ljava/lang/String;
       4: ldc           #36                 // String ISO-8859-1
       6: invokevirtual #42                 // Method java/lang/String.getBytes:(Ljava/lang/String;)[B
       9: aload_1
      10: invokestatic  #49                 // Method org/jruby/ir/runtime/IRRuntimeHelpers.decodeScopeFromBytes:(Lorg/jruby/Ruby;[BLjava/lang/String;)Lorg/jruby/ir/IRScope;
      13: areturn
</code></pre></div></div>

<h1 id="future-topics">Future Topics</h1>

<p>I’m trying to do more frequent, compact blog posts, so I’m not going to get into the more elaborate generation of deployable web archives today. Here’s a few topics I’ll try to cover in future posts:</p>

<ul>
  <li>Generating deployable WAR files from any rack-compatible web application.</li>
  <li>Including jar dependencies from the JVM ecosystem as part of your tool.</li>
  <li>Getting around the JVM requirement: using JVM packaging tools to combine your jar with a complete runnable JVM environment.</li>
</ul>

<p>A good friend of the JRuby project – Mohit Sindhwani – also <a href="https://notepad.onghu.com/2021/jruby-win-day2-creating-jar-files/">blogged about Warbler back in 2021</a>. His company and many others use Warbler for commercial packaging of Ruby applications every day, ranging from simple command-line utilities all the way up to enterprise scale applications.</p>

<p>The Warbler project just had its first major release in almost a decade, so we’re currently updating it for modern JRuby, Ruby, and JVM features and requirements. There’s a lot of room for cleanup and new features!</p>

<p>If you’re interested in packaging your Ruby tools for easy, secure distribution to friends, family, or customers… please give Warbler a try and let us know how we can help you!</p>

<p>https://github.com/jruby/warbler</p>

<h2 id="join-the-discussion-on-rruby"><a href="https://www.reddit.com/r/ruby/comments/1ocje8h/packaging_ruby_apps_with_warbler_executable_jar/">Join the discussion on r/ruby!</a></h2>

<h1 id="jruby-support-for-your-project"><em>JRuby Support for Your Project</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>JRuby development is made possible by our primary sponsor, <a href="https://headius.com">Headius Enterprises</a>, offering a range of professional development and support resources for your team. You can choose JRuby for your next project knowing that you’ve got the world’s best JRuby experts standing by to help.</p>

<p>If you are interested in <strong>scaling Ruby</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of JVM features like <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, you need to be using JRuby. Let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>

<p>If you find this article or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[Warbler is the JRuby ecosystem’s tool for packaging up Ruby apps with all dependencies in a single deployable file. We’ve just released an update, so let’s explore how to use Warbler to create all-in-one packaged Ruby apps!]]></summary></entry><entry><title type="html">Updating Deprecations with Version Information</title><link href="https://headius.github.io/2025/10/updating-deprecations-with-version.html" rel="alternate" type="text/html" title="Updating Deprecations with Version Information" /><published>2025-10-10T00:00:00+00:00</published><updated>2025-10-10T00:00:00+00:00</updated><id>https://headius.github.io/2025/10/updating-deprecations-with-version</id><content type="html" xml:base="https://headius.github.io/2025/10/updating-deprecations-with-version.html"><![CDATA[<p>Java 9 added the ability to mark a <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotation with a “since” version, so we figured it was worth updating JRuby.</p>

<h1 id="deprecation-in-java">Deprecation in Java</h1>

<p>Deprecation is the process of marking a feature as “no longer supported” or “on its way out”, usually in a programmatic way so users can see warnings at build time. Nearly all languages and ecosystems have some way to mark features, APIs, or libraries as deprecated.</p>

<p>In Java 1.4 and earlier, this was done via the <code class="language-plaintext highlighter-rouge">@deprecated</code> directive (note lower-case “d”) in JavaDoc, which was great for documentation users but required extra processing by source-level tools and was not visible to any tools at runtime.</p>

<p>Java 1.5 introduced a new annotation <code class="language-plaintext highlighter-rouge">java.lang.Deprecated</code> that is retained at runtime and requires only annotation-awareness to process. It has become a generally preferred way to mark classes, methods, and fields as deprecated. Unfortunately, it provided no additional information, like a date or version after which the feature should not be used, or whether it would eventually be removed.</p>

<p>Java 9 fixed that, by adding two attributes to the <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotation: <code class="language-plaintext highlighter-rouge">since</code>, indicating when the deprecation first went into effect and <code class="language-plaintext highlighter-rouge">forRemoval</code>, indicating that the feature has been scheduled for removal. The <code class="language-plaintext highlighter-rouge">since</code> attribute is only informational, but a setting of <code class="language-plaintext highlighter-rouge">forRemoval=true</code> tells the compiler to complain about uses even more loudly (and perhaps fail the build).</p>

<h1 id="deprecation-in-jruby">Deprecation in JRuby</h1>

<p>The modern era of JRuby extends back almost to the release of Java 1.5, so we’ve steadily added <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotations throughout our codebase as we move away from old features and peculiarly-named methods. These deprecations are generally not visible to the average JRuby user, but if you use JRuby as an API or embed it into a larger Java application, you’ll get compile-time warnings for deprecated features.</p>

<p>With the release of JRuby 10, we now depend on a minimum of Java 21… which means we can also use the <code class="language-plaintext highlighter-rouge">since</code> tag. But what of all the existing deprecations?</p>

<h2 id="updating-deprecations-with-since">Updating deprecations with “since”</h2>

<p>This week, we decided it would be worth rewriting all “bare” <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotations to include <code class="language-plaintext highlighter-rouge">since</code> information. The process required a few steps:</p>

<ul>
  <li>Get the source locations of all <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotations that did not contain any attributes.</li>
  <li>For each location, determine the first JRuby release in which the deprecation was shipped.</li>
  <li>Update the deprecations to point at that version.</li>
</ul>

<p>With a little Ruby and Git, here’s the script I came up with:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># scan all .java files in the dir specified by ARGV[0]</span>
<span class="no">Dir</span><span class="p">[</span><span class="no">ARGV</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="s2">"/**/*.java"</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
  <span class="c1"># read all lines from the file</span>
  <span class="n">file_lines</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">readlines</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
  
  <span class="n">file_lines</span><span class="p">.</span><span class="nf">each_with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">line</span><span class="p">,</span> <span class="n">i</span><span class="o">|</span>
    <span class="n">line</span><span class="p">.</span><span class="nf">gsub!</span><span class="p">(</span><span class="sr">/@Deprecated\S*$/</span><span class="p">)</span> <span class="k">do</span>
      <span class="c1"># use git blame to find the commit that introduced the annotation</span>
      <span class="n">sha</span> <span class="o">=</span> <span class="sb">`git blame -L </span><span class="si">#{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="sb">,</span><span class="si">#{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="sb"> </span><span class="si">#{</span><span class="n">file</span><span class="si">}</span><span class="sb">`</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
      
      <span class="c1"># use git describe to get the first tag that contains that commit</span>
      <span class="n">tag</span> <span class="o">=</span> <span class="sb">`git describe --abbrev=0 --tags --contains </span><span class="si">#{</span><span class="n">sha</span><span class="si">}</span><span class="sb">`</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"~"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
      
      <span class="c1"># add that tag to a "since" attribute for the annotation</span>
      <span class="s2">"@Deprecated(since = </span><span class="se">\"</span><span class="si">#{</span><span class="n">tag</span><span class="si">}</span><span class="se">\"</span><span class="s2">)"</span>
    <span class="k">end</span>
  <span class="k">end</span>
  
  <span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">file_lines</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">''</span><span class="p">))</span>
  <span class="nb">puts</span> <span class="s2">"processed </span><span class="si">#{</span><span class="n">file</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="ups-and-downs">Ups and downs</h2>

<p>The PR I created is here: https://github.com/jruby/jruby/pull/9027</p>

<p>On the up side, this clearly indicates when each deprecation was added, so we can start to remove the oldest once (for some definition of “oldest”). It also gives better output to users so they know how long the feature they’re using has been deprecated.</p>

<p>On the down side, I had to manually fix several tags that were bogus or inaccurate (which maybe is a plus, really), and a piece of code was ever moved then the history is likely broken and the “since” version might be inaccurate. A improved version might also try to track file moves, but that was more than I needed here.</p>

<p>There’s also a small concern that we’ve basically just updated all of those <code class="language-plaintext highlighter-rouge">@Deprecated</code> lines, so any future blaming will have to dig deeper to find out when and why they were deprecated. But I kept this change as a single commit, so at worst you’ll have to <code class="language-plaintext highlighter-rouge">git blame</code> the parent commit in such cases.</p>

<h2 id="what-do-you-think">What do you think?</h2>

<p>Before I merged this, I asked if anyone could think of a good reason not to do it… and I got no responses. What do you think of this rewriting of <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotations to include the version?</p>

<p>If you decide to proceed with a similar transformation, I hope this script is helpful! Remember you can always just run it with JRuby, and the easiest way to run JRuby for a simple script would be to use <a href="https://www.jbang.dev/">jbang</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby $ jbang run@jruby deprecated_since.rb core/src/main/java
processed core/src/main/java/org/jruby/AbstractRubyMethod.java
processed core/src/main/java/org/jruby/Appendable.java
processed core/src/main/java/org/jruby/BasicObjectStub.java
processed core/src/main/java/org/jruby/DelegatedModule.java
processed core/src/main/java/org/jruby/EvalType.java
...
</code></pre></div></div>

<p>Enjoy!</p>

<h2 id="join-the-discussion-on-rruby"><a href="https://www.reddit.com/r/ruby/comments/1o3eiid/updating_jrubys_deprecations_with_since_version/">Join the discussion on r/ruby!</a></h2>

<h2 id="join-the-discussion-on-rjava"><a href="https://www.reddit.com/r/java/comments/1o3eku0/updating_historical_deprecations_with_since/">Join the discussion on r/java!</a></h2>

<h1 id="jruby-support-for-your-project"><em>JRuby Support for Your Project</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>JRuby development is made possible by our primary sponsor, <a href="https://headius.com">Headius Enterprises</a>, offering a range of professional development and support resources for your team. You can choose JRuby for your next project knowing that you’ve got the world’s best JRuby experts standing by to help.</p>

<p>If you are interested in <strong>scaling Ruby</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of JVM features like <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, you need to be using JRuby. Let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>

<p>If you find this article or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[Java 9 added the ability to mark a @Deprecated annotation with a “since” version, so we figured it was worth updating JRuby.]]></summary></entry><entry><title type="html">JRuby and Leyden: Even Better Startup</title><link href="https://headius.github.io/2025/09/jruby-jdk25-startup-time-follow-up.html" rel="alternate" type="text/html" title="JRuby and Leyden: Even Better Startup" /><published>2025-09-26T00:00:00+00:00</published><updated>2025-09-26T00:00:00+00:00</updated><id>https://headius.github.io/2025/09/jruby-jdk25-startup-time-follow-up</id><content type="html" xml:base="https://headius.github.io/2025/09/jruby-jdk25-startup-time-follow-up.html"><![CDATA[<p>At the end of my post on JRuby and JDK 25 startup time features, I teased a bit of the unreleased improvements from <a href="https://openjdk.org/projects/leyden/">Project Leyden</a>. It turns out the latest commits improve startup time <strong>even more</strong>, so it seems worth posting a quick follow-up!</p>

<h1 id="project-leyden-is-lit">Project Leyden is LIT</h1>

<p>Of the many OpenJDK projects I follow, Leyden has been near the top as far as activity and interest. In the past month, there’s been 527 commits to all branches… over 15 commits per day. And this doesn’t include commits being done by contributors on their own repositories. It’s exciting to watch!</p>

<p>After my recent post, <a href="https://bsky.app/profile/shipilev.bsky.social">Aleksey Shipilëv</a> reached out to me on Bluesky:</p>

<p><a href="https://bsky.app/profile/shipilev.bsky.social/post/3lzm3ss3fjs22"><img src="/images/aleksey_leyden_followup.png" alt="Aleksey Shipilëv Bluesky post about recent Leyden improvements" /></a></p>

<p>If you know Aleksey, you know to listen when he makes a suggestion. Naturally, I got to work building my own “premain” Leyden JDK build to try things out.</p>

<h2 id="baseline-startup">Baseline Startup</h2>

<p>To refresh your memory, our previous best time — with AOTCache and JRuby’s “dev” mode (which disables many optimizations) — was a lovely little <strong>423ms</strong>, down from an unaided 943ms.</p>

<p>We’ll try our baseline startup time training again, but this time I’ll leave off the <code class="language-plaintext highlighter-rouge">--dev</code> flag and let JRuby and the JVM fully optimize and record everything.</p>

<p>(Recall that the <code class="language-plaintext highlighter-rouge">--nocache</code> flag turns off JRuby’s automatic use of AppCDS, which is incompatible with AOTCache.)</p>

<p>Same training run:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby --nocache -J-XX:AOTCacheOutput=jruby.aot baseline_training.rb 
...
Reading AOTConfiguration jruby.aot.config and writing AOTCache jruby.aot
AOTCache creation is complete: jruby.aot 110788608 bytes
Removed temporary AOT configuration file jruby.aot.config
</code></pre></div></div>

<p>Note this new AOTCache is 110MB, a full 50MB larger than the 60MB from before. There must be something good in there… let’s try it.</p>

<p>(To make things a bit easier to read, I’ll be using <code class="language-plaintext highlighter-rouge">TIMEFMT="run time: %mE"</code> to format zsh’s <code class="language-plaintext highlighter-rouge">time</code> command.)</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time jruby --nocache -J-XX:AOTCache=jruby.aot -e 1
run time: 353ms
</code></pre></div></div>

<p>With the latest Leyden “premain” patches, JRuby’s base startup — sacrificing no features and no optimizations — is down to <strong>353ms</strong>, over 16% faster than our previous best!</p>

<h2 id="gem-list">gem list</h2>

<p>One thing I didn’t show in my previous post was the performance of <code class="language-plaintext highlighter-rouge">gem list</code> without explicitly training the AOTCache for <code class="language-plaintext highlighter-rouge">gem list</code>. Recall that without any help, <code class="language-plaintext highlighter-rouge">gem list</code> for 88 gems took <strong>1546ms</strong>, and our best time was <strong>815ms</strong>.</p>

<p>Here’s a <code class="language-plaintext highlighter-rouge">gem list</code> time with the latest Leyden “premain” AOTCache patches, trained only for baseline JRuby startup:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
run time: 919ms
</code></pre></div></div>

<p>Not bad! A single, simple training run has reduced <code class="language-plaintext highlighter-rouge">gem list</code> time by 40%, and we’ve achieved a sub-second run time without specifically training the AOTCache for this command. This means you can use a simple training run for your JRuby development environment and still see a massive improvement in startup time for arbitrary Ruby commands.</p>

<p>Of course, we want to see what happens if we specifically train the updated AOTCache for <code class="language-plaintext highlighter-rouge">gem list</code>. Targeted training like is useful when you have one type of command you must run repeatedly, or in a serverless Amazon Lambda-style environment that needs to quickly launch a small tool.</p>

<p>The training run proceeds as before:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby --nocache -J-XX:AOTCacheOutput=jruby.aot gem_list_training.rb
...
Reading AOTConfiguration jruby.aot.config and writing AOTCache jruby.aot
AOTCache creation is complete: jruby.aot 175816704 bytes
Removed temporary AOT configuration file jruby.aot.config
</code></pre></div></div>

<p>And here’s the <code class="language-plaintext highlighter-rouge">gem list</code> run time with our custom <code class="language-plaintext highlighter-rouge">AOTCache</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
run time: 714ms
</code></pre></div></div>

<p>Another new record: we’re down to <strong>714ms</strong>, beating the old record of 825ms by 13%, and we <strong>haven’t disabled any optimizations</strong>.</p>

<h1 id="whats-next">What’s Next?</h1>

<h2 id="watch-this-space">Watch This Space</h2>

<p>Even with these incredible results, I want to reiterate that it’s early days for Project Leyden.</p>

<p>We still haven’t achieved a “cold” <code class="language-plaintext highlighter-rouge">gem list</code> run time comparable to the 280ms from running it in-process in a loop, so clearly there’s optimizations happening that aren’t getting saved and restored.</p>

<p>And some of these AOTCache sizes might be prohibitively large; an uncached filesystem will take time to load up 100-200MB of AOTCache data, and that will be even worse if it’s loaded across the network.</p>

<p>But wow it’s exciting stuff. I have been telling JRuby users for years that “it’s not our fault” and “the JVM folks are working on it”. Finally, the dream of fast JRuby startup is coming true.</p>

<h2 id="other-options">Other Options</h2>

<p>My previous post neglected to mention other options for improving JRuby startup, like the <a href="https://blog.headius.com/2024/09/jruby-on-crac-part-1-lets-get-cracking.html">Project CRaC “checkpoint and restore” feature</a> I blogged about previously and alternative JVMs like <a href="https://developer.ibm.com/languages/semeru-runtimes/">IBM Semeru</a> (aka “OpenJ9”) and its <a href="https://www.ibm.com/docs/en/semeru-runtime-ce-z/17.0.0?topic=sharing-introduction">Shared Class Cache</a> and <a href="https://www.ibm.com/docs/en/semeru-runtime-ce-z/17.0.0?topic=documentation-jitserver-technology">JITServer</a> features. We’ll explore those and other options in an upcoming post!</p>

<h2 id="join-the-discussion-on-reddit"><a href="https://www.reddit.com/r/ruby/comments/1nr630x/jruby_and_leyden_even_better_startup/">Join the discussion on Reddit!</a></h2>

<h1 id="jruby-support-for-your-project"><em>JRuby Support for Your Project</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>JRuby development is made possible by our primary sponsor, <a href="https://headius.com">Headius Enterprises</a>, offering a range of professional development and support resources for your team. You can choose JRuby for your next project knowing that you’ve got the world’s best JRuby experts standing by to help.</p>

<p>If you are interested in <strong>scaling Ruby</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of JVM features like <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, you need to be using JRuby. Let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>

<p>If you find this article or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[At the end of my post on JRuby and JDK 25 startup time features, I teased a bit of the unreleased improvements from Project Leyden. It turns out the latest commits improve startup time even more, so it seems worth posting a quick follow-up!]]></summary></entry><entry><title type="html">JRuby and JDK 25: Startup Time with AOTCache</title><link href="https://headius.github.io/2025/09/jruby-jdk25-startup-time-with-aotcache.html" rel="alternate" type="text/html" title="JRuby and JDK 25: Startup Time with AOTCache" /><published>2025-09-24T00:00:00+00:00</published><updated>2025-09-24T00:00:00+00:00</updated><id>https://headius.github.io/2025/09/jruby-jdk25-startup-time-with-aotcache</id><content type="html" xml:base="https://headius.github.io/2025/09/jruby-jdk25-startup-time-with-aotcache.html"><![CDATA[<p><a href="https://openjdk.org/projects/jdk/25/">JDK 25</a> is the newest LTS release since JDK 21, and it ships with a gaggle of amazing VM-level features. This post will cover one of the most important improvements for command-line ecosystems like JRuby’s: the AOTCache (ahead-of-time cache) and its ability to pre-optimize code for future runs.</p>

<p>We’ll explore how AOTCache can speed up your JRuby workflow, starting with a discussion of JRuby startup time challenges and finishing with “coming soon” features that didn’t quite make it into the JDK 25 release.</p>

<h1 id="the-challenge-of-fast-startup-on-the-jvm">The Challenge of Fast Startup on the JVM</h1>

<p>It’s worth taking a quick look at why startup time has been such a difficult challenge for JRuby, and how we’ve worked to improve it over the years.</p>

<h2 id="why-does-jruby-start-up-slowly">Why does JRuby start up slowly?</h2>

<p>Whenever folks ask me “what’s the main issue I’ll have migrating to JRuby” the answer is almost always “startup time”. If the startup time problem had been solved for us 20 years ago, JRuby would already be the de-facto production Ruby for most developers. So why haven’t we just fixed it?</p>

<p>The unfortunate answer is that it’s really not our fault.</p>

<h2 id="starting-from-scratch">Starting from Scratch</h2>

<p><img src="/images/jruby_architecture.001.png" alt="JRuby architecture diagram" /></p>

<p>There’s many things about Ruby, JRuby, and the JVM that contribute to slow startup:</p>

<ul>
  <li>Ruby applications typically start from uncompiled source every time you run a command, and most of these commands load dozens or hundreds of source files. We have to re-parse and re-compile all that code every time.</li>
  <li>JRuby’s internal representation of Ruby code starts out interpreted, to reduce the overhead of optimizing code that’s only run once. We delay full optimization until we know methods will be used repeatedly.</li>
  <li>JRuby itself is compiled Java bytecode, which must be loaded into the JVM from the filesystem and interpreted by the JVM. Like JRuby’s mixed-mode runtime, the JVM defers optimization of that code until it can analyze and profile code. This means the JRuby parser, compiler, interpreter, core classes, and even our JIT compiler all start out “cold”.</li>
  <li>The JDK itself is largely written in Java, so even the platform JRuby is built upon takes some time to optimize.</li>
</ul>

<p>Even though we’ve made great improvements to JRuby’s startup, like [reducing memory allocation] and [deferring initialization of rarely-used state], we’re fighting an uphill battle. Most of the startup delay is due to the JVM itself.</p>

<p>Here’s JRuby 10 running a simple command line with no startup-time help on <strong>JDK 21</strong>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby -e 1  1.63s user 0.08s system 181% cpu 0.943 total
</code></pre></div></div>

<p>It’s not the end of the world, but <strong>943ms</strong> to basically run “hello, world” isn’t great.</p>

<h2 id="comparing-with-cruby">Comparing with CRuby</h2>

<p>Where JRuby has many layers of optimization, CRuby starts out with much of its code already optimized:</p>

<ul>
  <li>The parser, compiler, interpreter, and most of the core classes are written in C and <strong>start</strong> as native code.</li>
  <li>The CRuby boot process has been refined for 30 years to start up as fast as possible.</li>
</ul>

<p>As a result, CRuby takes about <strong>45ms</strong> for its baseline startup (around 20x faster than JRuby’s time above).</p>

<p>This design does put some hard limits on CRuby’s runtime optimizations, but it also sets a high bar for JRuby. Challenge accepted! Let’s see what we can do to narrow that gap.</p>

<h2 id="what-happens-once-the-jvm-optimizes-jruby">What happens once the JVM optimizes JRuby?</h2>

<p>Fortunately for JRuby users, the JVM doesn’t take <em>too</em> long to kick in, and once it does we can start to see the true performance of JRuby.</p>

<p>Let’s try a more real-world example, running a fairly heavy command: <code class="language-plaintext highlighter-rouge">gem list</code> with about 88 gems.</p>

<p><code class="language-plaintext highlighter-rouge">gem_list_timing.rb</code></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">20</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span>
  <span class="n">t</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span>
  <span class="n">org</span><span class="p">.</span><span class="nf">jruby</span><span class="o">.</span><span class="no">Main</span><span class="p">.</span><span class="nf">main</span><span class="p">(</span><span class="sx">%w[-S gem list]</span><span class="p">.</span><span class="nf">to_java</span><span class="p">(</span><span class="ss">:string</span><span class="p">))</span>
  <span class="vg">$stderr</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"iteration </span><span class="si">#{</span><span class="n">_1</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="o">-</span> <span class="n">t</span><span class="si">}</span><span class="s2">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This simple script runs in JRuby and basically just re-launches additional JRuby instances in the same process. Here’s the output of timings from <code class="language-plaintext highlighter-rouge">stderr</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby gem_list_timing.rb &gt; /dev/null
iteration 0: 1.0590579999999998
iteration 1: 0.435281
iteration 2: 0.39674800000000005
iteration 3: 0.40792199999999995
iteration 4: 0.358851
iteration 5: 0.327706
iteration 6: 0.356299
iteration 7: 0.383793
iteration 8: 0.306416
iteration 9: 0.29007
iteration 10: 0.301724
iteration 11: 0.391308
iteration 12: 0.302654
iteration 13: 0.2939
iteration 14: 0.300466
iteration 15: 0.301517
iteration 16: 0.285803
iteration 17: 0.316092
iteration 18: 0.28180099999999997
iteration 19: 0.280565
</code></pre></div></div>

<p>Even though the first iteration here is actually the <strong>second</strong> time JRuby has booted in this process (the JRuby running the script precedes it), we’re still taking over a second just to list installed gems. But the JVM optimizations kick in quickly, with the second iteration taking half as much time and the final iteration (still getting faster) taking a bit over one-quarter as much time.</p>

<h1 id="improving-startup-one-step-at-a-time">Improving startup one step at a time</h1>

<p>Clearly, JRuby can run simple commands fast once the JVM gets going. Our challenge has been finding ways to shorten that curve without sacrificing JRuby functionality. Let’s explore a few of these tricks.</p>

<h2 id="technique-1-do-less">Technique #1: Do Less</h2>

<p>One of the simplest ways to eliminate startup-time overhead is to simply do less of it.</p>

<p>You can see in the diagram above that there are many phases to running JRuby code on the JVM:</p>

<ul>
  <li>We parse and compile Ruby to our intermediate representation (IR)</li>
  <li>We interpret that Ruby IR for a while</li>
  <li>Eventually, we compile our IR into JVM bytecode</li>
  <li>The JVM interprets that bytecode for a while</li>
  <li>An early, simple optimizer in the JVM turns that bytecode into semi-optimized native code</li>
  <li>That native code continues to be profiled and analyzed</li>
  <li>A later, profile-driven optimizer recompiles the bytecode into optimized native code</li>
</ul>

<p>Of course by the time we reach that last stage, short commands may already have exited, and we’ve wasted a lot of effort optimizing code that hardly gets run.</p>

<p>Realizing that many commands in a typical Ruby workflow don’t require the highest-performing execution, we introduced a development mode flag: <code class="language-plaintext highlighter-rouge">--dev</code></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time jruby -e 1      
jruby -e 1  1.63s user 0.08s system 181% cpu 0.941 total
$ time jruby --dev -e 1
jruby --dev -e 1  0.82s user 0.07s system 123% cpu 0.723 total
</code></pre></div></div>

<p>This example runs a trivial Ruby script that just evaluates the number <code class="language-plaintext highlighter-rouge">1</code> and exits. JRuby without any flags takes 941ms. When we use the <code class="language-plaintext highlighter-rouge">--dev</code> flag, however, that time reduces to 723ms. What’s happening here?</p>

<p>The <code class="language-plaintext highlighter-rouge">--dev</code> flag basically turns off several stages in the optimization pipeline, so we start running more quickly and avoid doing optimizations that won’t be useful:</p>

<ul>
  <li>JRuby’s bytecode compiler is disabled, so we just stay in the JRuby IR interpreter.</li>
  <li>The JVM’s second-stage profiled optimizer is disabled, so it just generates simple native code for JRuby’s internal logic and the rest of the JDK.</li>
  <li>There’s a <a href="https://github.com/jruby/jruby/blob/master/bin/.dev_mode.java_opts">few other tweaks</a> that also have a small impact.</li>
</ul>

<p>This has been, up until recently, the best way to improve JRuby startup. Let’s review a newer technique I blogged about a few months ago, the <a href="https://openjdk.org/jeps/310">Application Class Data Sharing</a>.</p>

<h2 id="technique-2-preprocess-jruby-and-jdk-code-with-appcds">Technique #2: Preprocess JRuby and JDK code with AppCDS</h2>

<p>Application Class Data Sharing (AppCDS) is an OpenJDK feature that allows you to save off some of the early boot-time work of loading and processing JVM class files, eliminating that processing for future runs. In my post <a href="https://blog.headius.com/2025/02/boosting-jruby-startup-with-appcds-and-aotcache.html">Boosting JRuby Startup with AppCDS and AOT caching</a>, I walk through the details of “AppCDS” and show how you can use it with JRuby.</p>

<p>Since then, we’ve released JRuby 10 and updates for JRuby 9.4 that will <strong>automatically</strong> use AppCDS to improve startup! This feature can be disabled with the <code class="language-plaintext highlighter-rouge">--nocache</code> flag. Let’s review  startup performance with and without AppCDS.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># JRuby with no startup help
$ time jruby --nocache -e 1
jruby --nocache -e 1  1.61s user 0.08s system 182% cpu 0.925 total

# JRuby with AppCDS (on by default)
$ time jruby -e 1
jruby -e 1  1.28s user 0.06s system 191% cpu 0.702 total

# JRuby with "dev" mode only
$ time jruby --nocache --dev -e 1
jruby --nocache --dev -e 1  0.83s user 0.07s system 125% cpu 0.717 total

# JRuby with "dev" mode and AppCDS
$ time jruby --dev -e 1
jruby --dev -e 1  0.63s user 0.04s system 118% cpu 0.567 total
</code></pre></div></div>

<p>Now we’re talking! With AppCDS, base JRuby startup in normal mode (with <strong>full optimization</strong> still enabled) drops down to 702ms, less than “dev” mode alone. And JRuby with both AppCDS <strong>and</strong> “dev” mode gets down to 567ms!</p>

<p>At these speeds, startup time becomes almost a non-issue, but what about a heavier command like <code class="language-plaintext highlighter-rouge">gem list</code>?</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># gem list with no startup help
$ time jruby --nocache -S gem list &gt; /dev/null
jruby --nocache -S gem list &gt; /dev/null  3.44s user 0.14s system 215% cpu 1.663 total

# gem list with AppCDS
$ time jruby -S gem list &gt; /dev/null
jruby -S gem list &gt; /dev/null  3.07s user 0.14s system 229% cpu 1.397 total

# gem list with "dev" mode only
$ time jruby --nocache --dev -S gem list &gt; /dev/null
jruby --nocache --dev -S gem list &gt; /dev/null  1.52s user 0.11s system 141% cpu 1.148 total

# gem list with "dev" mode and AppCDS
$ time jruby --dev -S gem list &gt; /dev/null
jruby --dev -S gem list &gt; /dev/null  1.31s user 0.09s system 145% cpu 0.967 total
</code></pre></div></div>

<p>Ok, for a more “real world” command line, we’re still slower than we’d like taking nearly a second just to list gems (similar to “iteration 0” in our looping test). We need to do better!</p>

<h2 id="technique-3-upgrade-the-jdk">Technique #3: Upgrade the JDK</h2>

<p>There’s a saying in the Java world: “If you want better performance… just wait a bit.” This reflects two truths about the JVM:</p>

<ul>
  <li>It might take a while to optimize, for many reasons spelled out above.</li>
  <li>Newer releases of the JDK almost always run faster than older versions.</li>
</ul>

<p>Let’s try the above AppCDS examples with <strong>JDK 25</strong> instead of JDK 21:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># gem list with no startup help
$ time jruby --nocache -S gem list &gt; /dev/null
jruby --nocache -S gem list &gt; /dev/null  3.30s user 0.14s system 222% cpu 1.546 total

# gem list with AppCDS
$ time jruby -S gem list &gt; /dev/null          
jruby -S gem list &gt; /dev/null  3.02s user 0.14s system 234% cpu 1.350 total

# gem list with "dev" mode only
$ time jruby --nocache --dev -S gem list &gt; /dev/null
jruby --nocache --dev -S gem list &gt; /dev/null  1.51s user 0.16s system 149% cpu 1.116 total

# gem list with "dev" mode and AppCDS
$ time jruby --dev -S gem list &gt; /dev/null
jruby --dev -S gem list &gt; /dev/null  1.40s user 0.13s system 158% cpu 0.966 total
</code></pre></div></div>

<p>There’s some modest improvements here just from upgrading:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">gem list</code> with no help reduces from 1663ms to 1546ms (7.1% improvement).</li>
  <li><code class="language-plaintext highlighter-rouge">gem list</code> with AppCDS reduces from 1397ms to 1350ms (3.4% improvement).</li>
  <li><code class="language-plaintext highlighter-rouge">gem list</code> with “dev” mode reduces from 1148ms to 1116ms (2.8% improvement).</li>
  <li><code class="language-plaintext highlighter-rouge">gem list</code> with both “dev” mode and AppCDS reduces from 0.967ms to 0.966ms (less than 1% improvement).</li>
</ul>

<p>We’re seeing gains, but they’re not super impressive. But JDK 25 has an ace up its sleeve: AOTCache.</p>

<h2 id="technique-4-save-optimization-data-across-runs">Technique #4: Save optimization data across runs</h2>

<p>The features in JDK 25 build on the capabilities of AppCDS and add several enhancements compared to JDK 21:</p>

<ul>
  <li>AOTCache replaces AppCDS as the primary mechanism for improving startup (<a href="https://openjdk.org/jeps/483">JEP 843</a>: “Ahead-of-Time Class Loading &amp; Linking” shipped in JDK 24)</li>
  <li>Creating an AOTCache is made easier, with only one “training” step required (<a href="https://openjdk.org/jeps/514">JEP 514</a>: “Ahead-of-Time Command-Line Ergonomics”)</li>
  <li>The AOTCache can now save optimization profiles from a training run to aid optimization of future runs (<a href="https://openjdk.org/jeps/515">JEP 515</a>: “Ahead-of-Time Method Profiling”)</li>
</ul>

<p>Let’s try switching from AppCDS to the AOTCache.</p>

<p>First, we need to do a training run to allow the JVM to record optimization profiles. We’ll run JRuby in a loop within a single process and save the resulting AOTCache.</p>

<p><strong>baseline_training.rb</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">100</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span>
  <span class="n">ruby</span> <span class="o">=</span> <span class="n">org</span><span class="p">.</span><span class="nf">jruby</span><span class="o">.</span><span class="no">Ruby</span><span class="p">.</span><span class="nf">new_instance</span>
  <span class="n">ruby</span><span class="p">.</span><span class="nf">eval_scriptlet</span> <span class="s2">"1"</span>
  <span class="nb">print</span> <span class="s2">"."</span>
  <span class="n">ruby</span><span class="p">.</span><span class="nf">tear_down</span>
<span class="p">}</span>
<span class="nb">puts</span>
</code></pre></div></div>

<p>This time, rather than running JRuby’s <code class="language-plaintext highlighter-rouge">main</code> entry point, we create new JRuby runtime instances in a loop, evaluate some code, and tear them down. Doing this 100 times gives the JVM a chance to “learn” how JRuby boots up and executes.</p>

<p>To train with AOTCache, we disable AppCDS (<code class="language-plaintext highlighter-rouge">--nocache</code>) and use the JVM flag <code class="language-plaintext highlighter-rouge">-XX:AOTCacheOutput=jruby.aot</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby --nocache -J-XX:AOTCacheOutput=jruby.aot baseline_training.rb
....................................................................................................
[5.678s][warning][aot] Skipping org/jruby/util/JDBCDriverUnloader: Duplicated unregistered class
[5.678s][warning][aot] Skipping org/jruby/util/JDBCDriverUnloader: Duplicated unregistered class
...
[5.681s][warning][aot] Skipping org/joda/time/base/BaseDateTime: Old class has been linked
[5.681s][warning][aot] Skipping jdk/proxy2/$Proxy28: Unsupported location
Temporary AOTConfiguration recorded: jruby.aot.config
Launching child process /Library/Java/JavaVirtualMachines/zulu-25.jdk/Contents/Home/bin/java to assemble AOT cache jruby.aot using configuration jruby.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=: --add-opens=java.base/java.io=org.jruby.dist --add-opens=java.base/java.nio.channels=org.jruby.dist --add-opens=java.base/sun.nio.ch=org.jruby.dist --add-opens=java.management/sun.management=org.jruby.dist -Xss2048k -Djffi.boot.library.path=/Users/headius/work/jruby/lib/jni -Djava.security.egd=file:/dev/urandom --enable-native-access=org.jruby.dist --sun-misc-unsafe-memory-access=allow --module-path=/Users/headius/work/jruby/lib/jruby.jar -Djruby.home=/Users/headius/work/jruby -Djruby.lib=/Users/headius/work/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh -XX:AOTCacheOutput=jruby.aot -XX:AOTConfiguration=jruby.aot.config -XX:AOTMode=create
Reading AOTConfiguration jruby.aot.config and writing AOTCache jruby.aot
[0.285s][warning][aot] Skipping org/joda/time/tz/FixedDateTimeZone: Unlinked class not supported by AOTClassLinking
...
[0.286s][warning][aot] Skipping org/joda/time/DateTimeZone: Unlinked class not supported by AOTClassLinking
AOTCache creation is complete: jruby.aot 60653568 bytes
Removed temporary AOT configuration file jruby.aot.config
$
</code></pre></div></div>

<p>Now, we can run our previous tests, passing the JVM flag <code class="language-plaintext highlighter-rouge">-XX:AOTCache=jruby.aot</code> to use the generated AOTCache file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># JRuby with AOTCache
$ time jruby --nocache -J-XX:AOTCache=jruby.aot -e 1
jruby --nocache -J-XX:AOTCache=jruby.aot -e 1  1.10s user 0.08s system 203% cpu 0.581 total

# JRuby with "dev" mode and AOTCache
$ time jruby --nocache --dev -J-XX:AOTCache=jruby.aot -e 1
jruby --nocache --dev -J-XX:AOTCache=jruby.aot -e 1  0.65s user 0.07s system 139% cpu 0.518 total
</code></pre></div></div>

<p>Wow! Baseline startup with AOTCache is almost as fast as AppCDS with “dev” mode (<strong>581ms</strong> vs 567ms), and AOTCache plus “dev” mode gives us our best time yet: <strong>518ms</strong>!</p>

<p>We can see that <code class="language-plaintext highlighter-rouge">gem list</code> has similar improvements:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># gem list with AOTCache
$ time jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null  2.69s user 0.13s system 244% cpu 1.152 total

# gem list with "dev" mode and AOTCache
$ time jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null  1.39s user 0.11s system 173% cpu 0.861 total
</code></pre></div></div>

<p>Another record… <code class="language-plaintext highlighter-rouge">gem list</code> with 88 gems is now comfortably less than a second, at <strong>861ms</strong>.</p>

<p>Can we do better by training our AOTCache using <code class="language-plaintext highlighter-rouge">gem list</code>?</p>

<p><strong>gem_list_training.rb</strong>*</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">100</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span>
  <span class="n">org</span><span class="p">.</span><span class="nf">jruby</span><span class="o">.</span><span class="no">Main</span><span class="p">.</span><span class="nf">main</span><span class="p">(</span><span class="sx">%w[-S gem list]</span><span class="p">.</span><span class="nf">to_java</span><span class="p">(</span><span class="ss">:string</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Yes we can!</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null  1.39s user 0.17s system 189% cpu 0.825 total
</code></pre></div></div>

<p>New record: <strong>825ms</strong>!</p>

<h2 id="technique-5-save-optimized-code">Technique #5: Save optimized code!</h2>

<p>But wait, there’s more!</p>

<p>The features of AOTCache that shipped with JDK 25 are obviously outstanding, and have delivered the best startup times yet for JRuby users. But the OpenJDK folks haven’t been standing still, and there’s even more coming!</p>

<p><a href="https://openjdk.org/projects/leyden/">Project Leyden</a> is the OpenJDK project where this work has been going on, and in late August they released a second Early Access build containing previews of that work. Several features that didn’t make the cut are available for testing and experimentation:</p>

<ul>
  <li><strong>AOT compilation of Java methods</strong>: Cache compiled native code.</li>
  <li><strong>AOT generation of Dynamic Proxies and reflection data</strong>: Cache optimized forms of commonly used JVM metaprogramming features.</li>
  <li><strong>AOT optimization for class lookup</strong>: Cache class lookup data to avoid doing the same lookups again.</li>
</ul>

<p>The best part? We can try out these features just by downloading <a href="https://mail.openjdk.org/pipermail/leyden-dev/2025-August/002586.html">Leyden EA2</a> and training the AOTCache as before.</p>

<p>I present to you the fastest JRuby startup times ever seen on a standard JDK:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># baseline startup with AOTCache (Leyden EA2)
$ time jruby --nocache -J-XX:AOTCache=jruby.aot -e 1
jruby --nocache -J-XX:AOTCache=jruby.aot -e 1  0.55s user 0.06s system 131% cpu 0.462 total

# baseline startup with "dev" mode and AOTCache (Leden EA2)
$ time jruby --nocache --dev -J-XX:AOTCache=jruby.aot -e 1
jruby --nocache --dev -J-XX:AOTCache=jruby.aot -e 1  0.46s user 0.06s system 122% cpu 0.423 total

# gem list with AOTCache (Leyden EA2)
$ time jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
jruby --nocache -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null  1.82s user 0.11s system 186% cpu 1.035 total

# gem list with "dev" mode and AOTCache (Leyden EA2)
$ time jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null
jruby --nocache --dev -J-XX:AOTCache=jruby.aot -S gem list &gt; /dev/null  1.07s user 0.09s system 142% cpu 0.815 total
</code></pre></div></div>

<p>JRuby baseline startup <strong>less than half a second</strong>!</p>

<p>Gem list down to only <strong>815ms</strong>!</p>

<p>Amazing!</p>

<h1 id="whats-next">What’s Next?</h1>

<h2 id="jruby-enhancements">JRuby Enhancements</h2>

<p>Using all our tricks and the latest enhancements to the JVM, we’ve managed to improve JRuby startup time by a huge amount:</p>

<ul>
  <li>Baseline startup went from 943ms to <strong>423ms</strong></li>
  <li><code class="language-plaintext highlighter-rouge">gem list</code> went from 1663ms to <strong>815ms</strong></li>
</ul>

<p>That’s over 50% faster startup across the board, with more improvements on the way!</p>

<p>Over the next few weeks, the JRuby team will be transitioning our automatic startup optimizations from AppCDS to AOTCache, to give users the best possible startup time for daily use. But you’ve already seen how you can experiment with these features today. Try it out and let us know how it goes!</p>

<h2 id="project-leyden-collaboration">Project Leyden Collaboration</h2>

<p>We’re also going to be collaborating with the Project Leyden team to share our findings and help them to improve the AOTCache. We’re obviously <strong>very excited</strong> about this work, since it may finally solve the #1 complaint about using JRuby.</p>

<p>What will the Ruby world be like when JRuby users can have true parallelism, world-class garbage collection, and fast straight-line performance <strong>without compromising startup time</strong>?</p>

<p>We’re about to find out.</p>

<h2 id="join-the-discussion-on-reddit"><a href="https://www.reddit.com/r/ruby/comments/1npiavg/jruby_and_jdk_25_startup_time_with_aotcache/">Join the discussion on Reddit!</a></h2>

<h1 id="jruby-support-for-your-project"><em>JRuby Support for Your Project</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p>JRuby development is made possible by our primary sponsor, <a href="https://headius.com">Headius Enterprises</a>, offering a range of professional development and support resources for your team. You can choose JRuby for your next project knowing that you’ve got the world’s best JRuby experts standing by to help.</p>

<p>If you are interested in <strong>scaling Ruby</strong> to new heights, deploying your applications in <strong>large enterprises</strong>, or taking advantage of JVM features like <strong>world-class JIT compilers</strong>, <strong>battle-tested libraries</strong>, and <strong>leading-edge AI tools</strong>, you need to be using JRuby. Let us help you.</p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>

<p>If you find this article or my work on JRuby to be interesting and useful, you can also sponsor me directly on GitHub. Your contributions help ensure the JRuby project keeps moving forward!</p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[JDK 25 is the newest LTS release since JDK 21, and it ships with a gaggle of amazing VM-level features. This post will cover one of the most important improvements for command-line ecosystems like JRuby’s: the AOTCache (ahead-of-time cache) and its ability to pre-optimize code for future runs.]]></summary></entry><entry><title type="html">3D Charts and More with JRuby and JFreeChart</title><link href="https://headius.github.io/2025/05/3d-charts-and-more-with-jruby-and-jfreechart.html" rel="alternate" type="text/html" title="3D Charts and More with JRuby and JFreeChart" /><published>2025-05-01T00:00:00+00:00</published><updated>2025-05-01T00:00:00+00:00</updated><id>https://headius.github.io/2025/05/3d-charts-and-more-with-jruby-and-jfreechart</id><content type="html" xml:base="https://headius.github.io/2025/05/3d-charts-and-more-with-jruby-and-jfreechart.html"><![CDATA[<p>After playing with JFreeChart and JRuby and writing up a lovely blog post called “<a href="https://blog.headius.com/2025/04/beautiful-charts-with-jruby-and-jfreechart.html">Beautiful Charts with JRuby and JFreeChart</a>” it seems that <a href="https://www.reddit.com/r/ruby/comments/1kbqh5l/creating_beautiful_charts_with_jruby_and/">some folks</a> did not agree with my assessment of the charts as “beautiful” and others did not realize the power of what they’d just seen.</p>

<p>So, let’s see if this one grabs your attention. As before, this example is available in the <a href="https://github.com/headius/jruby-charts">headius/jruby-charts</a> repository.</p>

<h1 id="orson-charts">Orson Charts</h1>

<p>Along with a comprehensive and configurable set of 2D chart formats, the JFreeChart project also ships <a href="https://github.com/jfree/orson-charts">Orson Charts</a>, an equally extensive set of 3D chart renderers. The library has been designed to work alongside JFreeChart and has all the same support for file output and GUI integration.</p>

<p>So you don’t think my bar chart was beautiful? Ok, maybe you’re right. Let’s kick it up a notch!</p>

<h2 id="everything-is-better-in-3d">Everything is Better in 3D</h2>

<p>It took me about 30 minutes to port one of the <a href="https://github.com/jfree/jfree-demos/tree/master/src/main/java/com/orsoncharts/demo">Orson Charts demos</a>, a 3D rendering of <a href="https://github.com/jfree/jfree-demos/blob/master/src/main/java/com/orsoncharts/demo/CategoryMarkerDemo2.java">quarterly revenues for four application monitoring companies</a>. I’ve made the following tweaks along the way:</p>

<ul>
  <li>Data is read in from a json file using the Ruby <code class="language-plaintext highlighter-rouge">json</code> library.</li>
  <li>The GUI and interactive elements have been removed.</li>
  <li>The chart is rendered to various file formats rather than to the screen.</li>
</ul>

<p>The full source is here: <a href="https://github.com/headius/jruby-charts/blob/master/examples/category_chart.rb">3D Bar Chart with JRuby and Orson Charts</a></p>

<p>Let’s jump in!</p>

<h2 id="adding-the-dependencies">Adding the Dependencies</h2>

<p>Orson Charts has a Maven artifact just like JFreeChart, so we add it to our <code class="language-plaintext highlighter-rouge">Jarfile</code> and re-lock the dependencies.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jar</span> <span class="s1">'org.jfree:jfreechart:1.5.5'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.chart3d:2.1.0'</span>
</code></pre></div></div>

<p>Locking is done with <code class="language-plaintext highlighter-rouge">lock_jars</code> as before.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby-charts $ lock_jars

-- jar root dependencies --

      org.jfree:jfreechart:1.5.5:compile
      org.jfree:org.jfree.chart3d:2.1.0:compile

Jars.lock updated
</code></pre></div></div>

<h2 id="importing-classes-for-cleaner-code">Importing Classes for Cleaner Code</h2>

<p>In my previous post, I mostly just referenced classes using their Java package + class format, but for this example we’ll import the classes ahead of time. This is an optional step; you can always just reference the classes directly.</p>

<p>If you want to be explicit about imports, you can use <code class="language-plaintext highlighter-rouge">java_import</code> which assigns a constant of the same name in the current namespace.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">java_import</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="o">.</span><span class="no">Color</span>
<span class="n">java_import</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="o">.</span><span class="no">Rectangle</span>
<span class="n">java_import</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="p">.</span><span class="nf">image</span><span class="o">.</span><span class="no">BufferedImage</span>
<span class="n">java_import</span> <span class="n">javax</span><span class="p">.</span><span class="nf">imageio</span><span class="o">.</span><span class="no">ImageIO</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="o">.</span><span class="no">Chart3DFactory</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">data</span><span class="o">.</span><span class="no">DefaultKeyedValues</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">data</span><span class="p">.</span><span class="nf">category</span><span class="o">.</span><span class="no">StandardCategoryDataset3D</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">interaction</span><span class="o">.</span><span class="no">StandardKeyedValues3DItemSelection</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">label</span><span class="o">.</span><span class="no">StandardCategoryItemLabelGenerator</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">legend</span><span class="o">.</span><span class="no">LegendAnchor</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart3d</span><span class="p">.</span><span class="nf">marker</span><span class="o">.</span><span class="no">CategoryMarker</span>
</code></pre></div></div>

<p>You can also just assign the constants yourself, if you prefer.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Color</span> <span class="o">=</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="o">.</span><span class="no">Color</span>
<span class="no">Rectangle</span> <span class="o">=</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="o">.</span><span class="no">Rectangle</span>
<span class="no">BufferedImage</span> <span class="o">=</span> <span class="n">java</span><span class="p">.</span><span class="nf">awt</span><span class="p">.</span><span class="nf">image</span><span class="o">.</span><span class="no">BufferedImage</span>
</code></pre></div></div>

<h2 id="generating-the-dataset-from-json">Generating the Dataset from JSON</h2>

<p>My previous post used canned data hand-entered into the dataset object, but of course you will typically read data from a database or json-formatted data source. I have created a <a href="https://github.com/headius/jruby-charts/blob/master/data/app_monitoring_revenue.json">json data file</a> based on the revenue demo’s data:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"New Relic"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Q2/19"</span><span class="p">:</span><span class="w"> </span><span class="mf">141.0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Q3/19"</span><span class="p">:</span><span class="w"> </span><span class="mf">146.0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Q4/19"</span><span class="p">:</span><span class="w"> </span><span class="mf">153.0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Q1/20"</span><span class="p">:</span><span class="w"> </span><span class="mf">160.0</span><span class="p">,</span><span class="w">
</span></code></pre></div></div>

<p>We can read it in and populate a <code class="language-plaintext highlighter-rouge">StandardCategoryDataset3D</code> using idiomatic Ruby code.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'json'</span>

<span class="n">dataset</span> <span class="o">=</span> <span class="no">StandardCategoryDataset3D</span><span class="p">.</span><span class="nf">new</span>
<span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="s2">"data/app_monitoring_revenue.json"</span><span class="p">))</span>
<span class="n">data</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">subset</span><span class="o">|</span>
  <span class="n">values</span> <span class="o">=</span> <span class="no">DefaultKeyedValues</span><span class="p">.</span><span class="nf">new</span>
  <span class="n">subset</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="n">values</span><span class="p">.</span><span class="nf">put</span><span class="p">(</span><span class="n">_1</span><span class="p">,</span> <span class="n">_2</span><span class="p">)</span> <span class="p">}</span>
  <span class="n">dataset</span><span class="p">.</span><span class="nf">add_series_as_row</span> <span class="nb">name</span><span class="p">,</span> <span class="n">values</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Let’s construct the chart now.</p>

<h2 id="creating-and-configuring-a-3d-bar-chart">Creating and Configuring a 3D Bar Chart</h2>

<p>The Orson Charts API feels familiar if you have used JFreeChart. This example does more customization and prettier rendering than my previous simple charts. Let’s walk through it.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">chart</span> <span class="o">=</span> <span class="no">Chart3DFactory</span><span class="p">.</span><span class="nf">create_bar_chart</span><span class="p">(</span>
  <span class="s2">"Quarterly Revenues"</span><span class="p">,</span>
  <span class="s2">"Application &amp; Performance Monitoring Companies"</span><span class="p">,</span>
  <span class="n">dataset</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s2">"Quarter"</span><span class="p">,</span>
  <span class="s2">"$million Revenues"</span><span class="p">)</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">chart_box_color</span> <span class="o">=</span> <span class="no">Color</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">127</span><span class="p">)</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">legend_anchor</span> <span class="o">=</span> <span class="no">LegendAnchor</span><span class="o">::</span><span class="no">BOTTOM_RIGHT</span>
</code></pre></div></div>

<p>We use <code class="language-plaintext highlighter-rouge">Chart3DFactory</code> to <code class="language-plaintext highlighter-rouge">create_bar_chart</code> with a title, subtitle, dataset, row, column, and value axis labels (row axis left empty).</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plot</span> <span class="o">=</span> <span class="n">chart</span><span class="p">.</span><span class="nf">plot</span>
<span class="n">plot</span><span class="p">.</span><span class="nf">gridline_paint_for_values</span> <span class="o">=</span> <span class="no">Color</span><span class="o">::</span><span class="no">BLACK</span>

<span class="n">renderer</span> <span class="o">=</span> <span class="n">plot</span><span class="p">.</span><span class="nf">renderer</span>
<span class="n">item_label_generator</span> <span class="o">=</span>
  <span class="no">StandardCategoryItemLabelGenerator</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">StandardCategoryItemLabelGenerator</span><span class="o">::</span><span class="no">VALUE_TEMPLATE</span><span class="p">)</span>
<span class="n">item_selection</span> <span class="o">=</span> <span class="no">StandardKeyedValues3DItemSelection</span><span class="p">.</span><span class="nf">new</span>
<span class="n">item_label_generator</span><span class="p">.</span><span class="nf">item_selection</span> <span class="o">=</span> <span class="n">item_selection</span>
<span class="n">renderer</span><span class="p">.</span><span class="nf">item_label_generator</span> <span class="o">=</span> <span class="n">item_label_generator</span>
</code></pre></div></div>

<p>Here we set the gridline color to black and set up a basic renderer for the bar labels to hide the numeric values (we’re visualizing the data, not trying to render every value in text).</p>

<p>That’s about it to get the basic 3D bar chart ready! The original example also includes features like highlighting a specific column, interactively browsing the data, and other features that are more suited for a rich GUI application.</p>

<h2 id="generating-output">Generating Output</h2>

<p>Let’s generate a PNG image from our chart. Unlike the previous examples, the <code class="language-plaintext highlighter-rouge">BarChart3D</code> class does not have a convenience method for creating and drawing into a <code class="language-plaintext highlighter-rouge">BufferedImage</code>, so we have a few extra lines of code here.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">width</span><span class="p">,</span> <span class="n">height</span> <span class="o">=</span> <span class="mi">600</span><span class="p">,</span> <span class="mi">500</span>
<span class="n">category_chart_image</span> <span class="o">=</span> <span class="no">BufferedImage</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="no">BufferedImage</span><span class="o">::</span><span class="no">TYPE_INT_RGB</span><span class="p">)</span>
<span class="n">category_chart_graphics</span> <span class="o">=</span> <span class="n">category_chart_image</span><span class="p">.</span><span class="nf">create_graphics</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">category_chart_graphics</span><span class="p">,</span> <span class="no">Rectangle</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
</code></pre></div></div>

<p>We create an RGB <code class="language-plaintext highlighter-rouge">BufferedImage</code> of the specified dimentions, get a <code class="language-plaintext highlighter-rouge">Graphics2D</code> object from it for writing, and tell the chart to draw itself into the specified area.</p>

<p>Now we can write it to a file.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">category_chart_file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"category_chart.png"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="no">ImageIO</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">category_chart_image</span><span class="p">,</span> <span class="s2">"PNG"</span><span class="p">,</span> <span class="n">category_chart_file</span><span class="p">.</span><span class="nf">to_outputstream</span><span class="p">)</span>
</code></pre></div></div>

<p>And we’ve got our <a href="/images/category_chart.png">chart</a>!</p>

<p><img src="/images/category_chart.png" alt="3D bar chart generated with JRuby and Orson Charts" /></p>

<p>Finally, we should at least pretend we’re writing production code and clean up the graphics context and the open file.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">category_chart_graphics</span><span class="p">.</span><span class="nf">dispose</span>
<span class="n">category_chart_file</span><span class="p">.</span><span class="nf">close</span>
</code></pre></div></div>

<h1 id="bonus-round">Bonus Round!</h1>

<p>A number of commenters also seemed to miss the real point of my article: this is just one of thousands of libraries available to JRuby users. Maybe you don’t want this chart as a PNG and want to render it in a browser as SVG? No problem!</p>

<h2 id="jfreecharts-svg-support">JFreeCharts SVG Support</h2>

<p>Using exactly the same API, you can also output SVG. First, we’ll add the additional dependency to our <code class="language-plaintext highlighter-rouge">Jarfile</code>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jar</span> <span class="s1">'org.jfree:jfreechart:1.5.5'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.chart3d:2.1.0'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.svg:5.0.6'</span>
</code></pre></div></div>

<p>After locking, we can require the libraries and render the exact same chart object to an SVG file.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">require_jar</span> <span class="s1">'org.jfree'</span><span class="p">,</span> <span class="s1">'org.jfree.svg'</span><span class="p">,</span> <span class="s1">'5.0.6'</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">svg</span><span class="o">.</span><span class="no">SVGGraphics2D</span>

<span class="n">svg_graphics</span> <span class="o">=</span> <span class="no">SVGGraphics2D</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="n">svg_graphics</span><span class="p">.</span><span class="nf">defs_key_prefix</span> <span class="o">=</span> <span class="s2">"jruby_charts"</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">element_hinting</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">svg_graphics</span><span class="p">,</span> <span class="no">Rectangle</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="n">svg</span> <span class="o">=</span> <span class="n">svg_graphics</span><span class="p">.</span><span class="nf">get_svg_element</span> <span class="n">chart</span><span class="p">.</span><span class="nf">id</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">"category_chart.svg"</span><span class="p">,</span> <span class="n">svg</span><span class="p">)</span>
</code></pre></div></div>

<p>And <a href="/images/category_chart.svg">there it is</a>!</p>

<p><img src="/images/category_chart.svg" alt="3D bar chart as SVG generated with JRuby and Orson Charts" /></p>

<p>But wait, there’s more!</p>

<h2 id="jfreecharts-pdf-support">JFreeCharts PDF Support</h2>

<p>PDF generation has typically been a struggle for CRuby users, with only a few working libraries, some abandoned and most incomplete. JRuby users, on the other hand, have a large number of full-featured PDF libraries available to them. In this case, JFreeChart provides its own PDF support that integrates directly into the same API.</p>

<p>We just add and lock the dependency in our <code class="language-plaintext highlighter-rouge">Jarfile</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jar</span> <span class="s1">'org.jfree:jfreechart:1.5.5'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.chart3d:2.1.0'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.svg:5.0.6'</span>
<span class="n">jar</span> <span class="s1">'org.jfree:org.jfree.pdf:2.0'</span>
</code></pre></div></div>

<p>And render the chart to <a href="/images/category_chart.pdf">PDF</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">require_jar</span> <span class="s1">'org.jfree'</span><span class="p">,</span> <span class="s1">'org.jfree.pdf'</span><span class="p">,</span> <span class="s1">'2.0.1'</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">pdf</span><span class="o">.</span><span class="no">PDFDocument</span>

<span class="n">pdf_doc</span> <span class="o">=</span> <span class="no">PDFDocument</span><span class="p">.</span><span class="nf">new</span>
<span class="n">pdf_doc</span><span class="p">.</span><span class="nf">title</span> <span class="o">=</span> <span class="s2">"Application &amp; Performance Monitoring Companies Revenue"</span>
<span class="n">pdf_doc</span><span class="p">.</span><span class="nf">author</span> <span class="o">=</span> <span class="s2">"Charles Oliver Nutter"</span><span class="p">;</span>
<span class="n">page</span> <span class="o">=</span> <span class="n">pdf_doc</span><span class="p">.</span><span class="nf">create_page</span><span class="p">(</span><span class="no">Rectangle</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">612</span><span class="p">,</span> <span class="mi">468</span><span class="p">))</span>
<span class="n">pdf_graphics</span> <span class="o">=</span> <span class="n">page</span><span class="p">.</span><span class="nf">graphics2D</span>
<span class="n">chart</span><span class="p">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">pdf_graphics</span><span class="p">,</span> <span class="no">Rectangle</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">612</span><span class="p">,</span> <span class="mi">468</span><span class="p">))</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">"category_chart.pdf"</span><span class="p">,</span> <span class="n">pdf_doc</span><span class="p">.</span><span class="nf">pdf_bytes</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="the-bottom-line">The Bottom Line</h1>

<p>The point of these posts is not to wow you with my <em>amazing</em> chart-designing skills. You can get your own hands dirty with these APIs and customize them to your heart’s content. I’m a <em>programmer</em>, not a graphic designer!</p>

<p>What I really want you get out of this is that it’s <strong>easy</strong> and <strong>fun</strong> to use these libraries in JRuby… without ever leaving Ruby. We’re talking about thousands of battle-tested, production ready tools that you can integrate into your applications <strong>today</strong>. No other Ruby implementation can give you that as quickly and easily.</p>

<p>JRuby is being actively developed by me and the rest of the JRuby team. We’re working full-time to deliver the best possible JVM Ruby runtime that we can. I just want to continue building tools that Rubyists like you will find useful!</p>

<p>So, what would you like to see next?</p>

<h2 id="join-the-discussion-on-reddit"><a href="https://www.reddit.com/r/ruby/comments/1kcbthz/3d_charts_svg_and_pdf_with_jruby_and_jfreechart/">Join the discussion on Reddit!</a></h2>

<h1 id="jruby-support-and-sponsorship"><em>JRuby Support and Sponsorship</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p><em>JRuby development is funded entirely through your generous sponsorships and the sale of commercial support contracts for JRuby developers and enterprises around the world. If you find my work exciting or believe it is important your company or your projects, please consider partnering with me to keep JRuby strong and moving forward!</em></p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[After playing with JFreeChart and JRuby and writing up a lovely blog post called “Beautiful Charts with JRuby and JFreeChart” it seems that some folks did not agree with my assessment of the charts as “beautiful” and others did not realize the power of what they’d just seen.]]></summary></entry><entry><title type="html">Creating Beautiful Charts with JRuby and JFreeChart</title><link href="https://headius.github.io/2025/04/beautiful-charts-with-jruby-and-jfreechart.html" rel="alternate" type="text/html" title="Creating Beautiful Charts with JRuby and JFreeChart" /><published>2025-04-30T00:00:00+00:00</published><updated>2025-04-30T00:00:00+00:00</updated><id>https://headius.github.io/2025/04/beautiful-charts-with-jruby-and-jfreechart</id><content type="html" xml:base="https://headius.github.io/2025/04/beautiful-charts-with-jruby-and-jfreechart.html"><![CDATA[<p>I recently returned from <a href="https://rubykaigi.org/2025/">RubyKaigi</a> where I had the opportunity to sit down with members of the Japanese Ruby community and show them a little bit of JRuby. One of the items that came up a few times was the difficulty of utilizing external libraries from Ruby: if it’s a C library, typically you have to either write a C extension or do the extra work of writing up an FFI binding.</p>

<p>If the library is not implemented in C or Ruby, things get even weirder.</p>

<p>One example is the <a href="https://github.com/red-data-tools/charty">Charty</a> library, one of the more popular options for generating beautiful chart graphics on Ruby. But Charty actually wraps a <em>Python</em> library called <a href="https://matplotlib.org/">matplotlib</a>, and the bindings for CRuby literally load Python into the current process and call it via Ruby C API calls which then make Python C API calls. The horror!</p>

<p>Also recently, a <a href="https://www.reddit.com/r/ruby/comments/1kalb7s/rails_action_mailer_rendering_charts_or_graphs_in/">Reddit Ruby post</a> demonstrated the use of the QuickChart library, which is implemented in JavaScript… yuck! In this case, the call-out is via process launching to the QuickChart command line, but either way it’s not at really integrated into the Ruby app using it.</p>

<p>Is there a better way? You bet there is: use a JVM-based library from JRuby! This post will show you how to get started with JRuby’s Java integration and a few quick examples of using the JFreeChart library from Ruby code. I promise you won’t have to write a single line of Java, C, Python, or JavaScript.</p>

<h1 id="jfreechart">JFreeChart</h1>

<p>The <a href="https://www.jfree.org/jfreechart/">JFreeChart library</a> provides an extensive set of chart formats, with a huge array of rendering options and easy integration with either JVM-based GUI toolkits or simple image-file output. Unlike many of the other charting libraries, it also supports live updating of a given chart GUI, making it an excellent choice for desktop monitoring tools.</p>

<p>Building a GUI with JRuby is fun and easy, but for this post we’ll focus on just the chart and image generation parts of the API and how you can use them easily from Ruby.</p>

<h1 id="jrubys-java-integration">JRuby’s Java Integration</h1>

<p>The magic starts with JRuby’s <a href="https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby">Java integration layer</a>. The simplest way to use a Java library is to basically pretend it’s a Ruby library, with a few tweaks along the way:</p>

<ul>
  <li>Download the library’s jar file and dependencies, or use jar-dependencies (part of JRuby’s standard library) to fetch them for you.</li>
  <li>Require the jar file and dependencies manually (a simple <code class="language-plaintext highlighter-rouge">require "myfile.jar"</code> works in JRuby!) or by using <code class="language-plaintext highlighter-rouge">require_jar</code> from <code class="language-plaintext highlighter-rouge">jar-dependencies</code>.</li>
  <li>Call the Java classes you’re interested from Ruby as if they were plain old Ruby classes. You can even import them, Java-style, but it’s often not necessary.</li>
</ul>

<p>Let’s start with a simple example.</p>

<h2 id="calling-into-the-javalangruntime-class">Calling into the java.lang.Runtime class</h2>

<p>Assuming you’ve installed <a href="https://www.jruby.org/2025/04/14/jruby-10-0-0-0.html">JRuby 10</a>, you can start playing with JVM libraries directly from IRB. Let’s fire it up and make sure we’re running on JRuby.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ irb
irb(main):001&gt; RUBY_ENGINE
=&gt; "jruby"
irb(main):002&gt;
</code></pre></div></div>

<p>The JDK comes with a large number of built-in features, of course, and I like to use the <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Runtime.html">java.lang.Runtime</a> class for quick demonstrations. It provides methods to query a bit of information about the running JVM and the host operating system. Let’s “import” it into our session and call a few methods.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>irb(main):002&gt; rt = java.lang.Runtime.runtime
=&gt; #&lt;Java::JavaLang::Runtime:0x39a87e72&gt;
irb(main):003&gt; rt.available_processors
=&gt; 8
irb(main):004&gt; rt.free_memory
=&gt; 222145360
</code></pre></div></div>

<p>There’s a few bits of magic here that make it really feel like Ruby:</p>

<ul>
  <li>JRuby defines a few top-level methods for common Java package roots like <code class="language-plaintext highlighter-rouge">java</code>, <code class="language-plaintext highlighter-rouge">javax</code>, <code class="language-plaintext highlighter-rouge">org</code>, and <code class="language-plaintext highlighter-rouge">com</code>. We use this to reference the Java package <code class="language-plaintext highlighter-rouge">java.lang</code> here and access the <code class="language-plaintext highlighter-rouge">Runtime</code> class.</li>
  <li><code class="language-plaintext highlighter-rouge">Runtime</code> defines a static method (similar to a Ruby class or singleton method) called <code class="language-plaintext highlighter-rouge">getRuntime</code> which gets the singleton instance of <code class="language-plaintext highlighter-rouge">Runtime</code> for the current JVM process. JRuby turns Java “getter” and “setter” methods into Ruby “attribute accessors”, so you can just call <code class="language-plaintext highlighter-rouge">Runtime.runtime</code> here.</li>
  <li>The Ruby calls to <code class="language-plaintext highlighter-rouge">available_processors</code> and <code class="language-plaintext highlighter-rouge">free_memory</code> translate to the Java methods <code class="language-plaintext highlighter-rouge">getAvailableProcessors</code> and <code class="language-plaintext highlighter-rouge">getFreeMemory</code> in the same way. The original methods are still callable, but we add aliases for the Ruby-friendly names.</li>
</ul>

<p>We can accomplish a tremendous amount with the libraries shipped in the JDK, ranging from simple database access to professional desktop GUI development.</p>

<p>If the libraries we want to use are not shipped with the JDK, however, we need to fetch them. It’s always possible to download the library’s “jar” file directly, but then you may also have to hunt down its dependency jars. A better way is to use JRuby’s <a href="https://github.com/jruby/jar-dependencies">jar-dependencies</a> library.</p>

<h2 id="jar-dependencies">jar-dependencies</h2>

<p>Like Ruby gems get pushed to <a href="https://rubygems.org/">rubygems.org</a>, Java libraries get published as jar files to <a href="https://maven.apache.org/">Maven</a> repositories, a global, federated repository of every version of every Java library known to man. In order to make it as easy as possible to use these libraries from JRuby, we’ve built extensive Maven tooling for both Ruby and Java applications.</p>

<p>The simplest way to load a JVM library from Maven into your JRuby app is to use <code class="language-plaintext highlighter-rouge">jar-dependencies</code>, a built-in tool for fetching and managing Java dependencies in either standalone applications or in gems.</p>

<p>For our examples, we want to fetch JFreeChart and its dependency libraries. If we search for it at <a href="https://search.maven.org">search.maven.org</a>, we can acquire its “Maven coordinates”.</p>

<p><img src="/images/maven_search_jfreechart.png" alt="Searching Maven Central for JFreeChart" /></p>

<p>The most recently-published artifact is jfreechart 1.5.5. From here we can see the coordinates we’re looking for:</p>

<ul>
  <li>The group ID, used for namespacing libraries, is <code class="language-plaintext highlighter-rouge">org.jfree</code>.</li>
  <li>The artifact ID, which identifies the library, is <code class="language-plaintext highlighter-rouge">jfreechart</code>.</li>
  <li>And the version we want is <code class="language-plaintext highlighter-rouge">1.5.5</code> (Maven strongly encourages always using specific versions).</li>
</ul>

<p>Given that, we can set up our JFreeChart project.</p>

<h1 id="jruby-and-jfreechart-happy-together">JRuby and JFreeChart: Happy Together!</h1>

<p>Without knowing anything about JFreeChart, I was able to load it up and get some simple examples working from IRB just last night. I was so impressed, I decided to write this blog post!</p>

<p>I’ve pushed this example as <a href="https://github.com/headius/jruby-charts">headius/jruby-charts</a> on GitHub so you can follow along.</p>

<h2 id="using-jar-dependencies-to-fetch-jfreechart">Using jar-dependencies to fetch JFreeChart</h2>

<p>We’ll start by creating a <code class="language-plaintext highlighter-rouge">jar-dependencies</code> “Jarfile”, which is roughly equivalent to Bundler’s “Gemfile”:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jar</span> <span class="s1">'org.jfree:jfreechart:1.5.5'</span>
</code></pre></div></div>

<p>Similar to a Gemfile’s gem name and version, we specify the Maven coordinates of the library we want (separated by colons). Once we have this file, we can fetch and “lock” this dependency with the <code class="language-plaintext highlighter-rouge">lock_jars</code> command.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby-charts $ lock_jars

-- jar root dependencies --

      org.jfree:jfreechart:1.5.5:compile

Jars.lock created
</code></pre></div></div>

<p>This command fetches JFreeChart and any other dependencies to your local Maven repository, which by default is in <code class="language-plaintext highlighter-rouge">~/.m2/repository</code>. In general, we recommend using jar-dependencies this way, so that your jars don’t conflict with other gems’ jars, and all applications on a given system use the same set of downloaded files. It’s also possible to fetch the jars and ship them inside your application or gem, but we’ll leave that example for another day.</p>

<p>Let’s get to the fun stuff: using JFreeChart from Ruby!</p>

<h2 id="generating-a-simple-bar-chart">Generating a simple bar chart</h2>

<p>Our first example will create a simple <a href="https://github.com/headius/jruby-charts/blob/master/examples/barchart.rb">bar chart</a>.</p>

<p>First, we need to load in the jars we just downloaded and locked.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Use jar-dependencies, included with JRuby, to load JFreeChart</span>
<span class="nb">require</span> <span class="s1">'jar-dependencies'</span>
<span class="n">require_jar</span> <span class="s1">'org.jfree'</span><span class="p">,</span> <span class="s1">'jfreechart'</span><span class="p">,</span> <span class="s1">'1.5.5'</span>
</code></pre></div></div>

<p>At this point <a href="https://www.jfree.org/jfreechart/javadoc/index.html">all of the classes of JFreeChart</a> are available to Ruby. We start by creating a <a href="https://www.jfree.org/jfreechart/javadoc/org/jfree/data/category/DefaultCategoryDataset.html">DefaultCategoryDataset</a> which will hold our bar chart data.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Create an empty CategoryDataSet</span>
<span class="n">bar_data</span> <span class="o">=</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">data</span><span class="p">.</span><span class="nf">category</span><span class="o">.</span><span class="no">DefaultCategoryDataset</span><span class="p">.</span><span class="nf">new</span>
</code></pre></div></div>

<p>JFreeChart provides a wide array of dataset types that can source data from a database, a JVM-based collection object (which includes Ruby collections), or other forms of structured data. The “default” version works nicely for a simple example. Let’s fill it with some data.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Add values to the dataset</span>
<span class="n">bar_data</span><span class="p">.</span><span class="nf">add_value</span> <span class="mi">44</span><span class="p">,</span> <span class="s2">"Ben and Jerry's"</span><span class="p">,</span> <span class="s2">"Flavors by creamery"</span>
<span class="n">bar_data</span><span class="p">.</span><span class="nf">add_value</span> <span class="mi">31</span><span class="p">,</span> <span class="s2">"Baskin Robbins"</span><span class="p">,</span> <span class="s2">"Flavors by creamery"</span>
<span class="n">bar_data</span><span class="p">.</span><span class="nf">add_value</span> <span class="mi">11</span><span class="p">,</span> <span class="s2">"Cold Stone"</span><span class="p">,</span> <span class="s2">"Flavors by creamery"</span>
</code></pre></div></div>

<p>This bar chart will display a count of ice cream flavors from three well-known purveyors of the creamery arts. The <code class="language-plaintext highlighter-rouge">add_value</code> method here is <code class="language-plaintext highlighter-rouge">addValue</code> in Java, and takes a number, a column key, and a row key.</p>

<p>Given our dataset, we can now request that JFreeChart create a basic bar chart for us using the <a href="https://www.jfree.org/jfreechart/javadoc/org/jfree/chart/ChartFactory.html">ChartFactory</a> class.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Create a bar chart with default settings</span>
<span class="n">java_import</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart</span><span class="o">.</span><span class="no">ChartFactory</span>
<span class="n">bar_chart</span> <span class="o">=</span> <span class="no">ChartFactory</span><span class="p">.</span><span class="nf">create_bar_chart</span> <span class="s2">"How Many Ice Cream Flavors?"</span><span class="p">,</span>
                                          <span class="s2">"Creamery"</span><span class="p">,</span> <span class="s2">"Flavors"</span><span class="p">,</span> <span class="n">bar_data</span>
</code></pre></div></div>

<p>We import the <code class="language-plaintext highlighter-rouge">ChartFactory</code> class for convenience (which is basically equivalent to doing <code class="language-plaintext highlighter-rouge">ChartFactory = org.jfree.chart.ChartFactory</code>) and then call <code class="language-plaintext highlighter-rouge">create_bar_chart</code> to generate a bar chart with default settings. The arguments we pass are the name of the chart, the label for the X axis, and the label for the Y axis, and our dataset.</p>

<p>Now that we have a chart, we can use Java’s graphics APIs (provided with the JDK by default) to output a PNG file.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Create a buffered image in memory at 500x500</span>
<span class="n">bar_image</span> <span class="o">=</span> <span class="n">bar_chart</span><span class="p">.</span><span class="nf">create_buffered_image</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">500</span>

<span class="c1"># Write the image as a PNG to a file</span>
<span class="n">bar_file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"barchart.png"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="n">javax</span><span class="p">.</span><span class="nf">imageio</span><span class="o">.</span><span class="no">ImageIO</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">bar_image</span><span class="p">,</span> <span class="s2">"PNG"</span><span class="p">,</span> <span class="n">bar_file</span><span class="p">.</span><span class="nf">to_outputstream</span><span class="p">)</span>
</code></pre></div></div>

<p>JFreeChart’s bar chart type knows how to create a Java2D <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/image/BufferedImage.html">BufferedImage</a>, which we can then pass to <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/javax/imageio/ImageIO.html">ImageIO</a> to write it out. In this case, we even use a Ruby <code class="language-plaintext highlighter-rouge">File</code> as the target, using a bit of JRuby magic that turns it into a Java <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/OutputStream.html">OutputStream</a>!</p>

<p>With our jar dependencies locked and our script written, we can simply run it with JRuby from a command line.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jruby-charts $ jruby examples/barchart.rb                      
2025-04-30 14:58:21.744 java[84148:2288876] +[IMKClient subclass]: chose IMKClient_Modern
2025-04-30 14:58:21.744 java[84148:2288876] +[IMKInputSession subclass]: chose IMKInputSession_Modern
</code></pre></div></div>

<p>Depending on your platform, you may see a little bit of “helpful” JVM output indicating that the graphics subsystem has been loaded.</p>

<p>Let’s see what we just made!</p>

<p><img src="/images/barchart.png" alt="A bar chart generated by JRuby and JFreeChart" /></p>

<p>Just a few lines of code and we’re done! Such fun!</p>

<h2 id="everyone-loves-pie">Everyone loves pie!</h2>

<p>The Java integration walkthrough and bar chart example above should whet your appetite for doing cool things with JRuby, but I include a <a href="https://github.com/headius/jruby-charts/blob/master/examples/piechart.rb">pie chart example</a> here to show a few differences:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Use jar-dependencies, included with JRuby, to load JFreeChart</span>
<span class="nb">require</span> <span class="s1">'jar-dependencies'</span>
<span class="n">require_jar</span> <span class="s1">'org.jfree'</span><span class="p">,</span> <span class="s1">'jfreechart'</span><span class="p">,</span> <span class="s1">'1.5.5'</span>

<span class="c1"># Create an empty PieDataset</span>
<span class="n">pie_data</span> <span class="o">=</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">data</span><span class="p">.</span><span class="nf">general</span><span class="o">.</span><span class="no">DefaultPieDataset</span><span class="p">.</span><span class="nf">new</span>

<span class="c1"># Add values to the dataset</span>
<span class="n">pie_data</span><span class="p">.</span><span class="nf">insert_value</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">"Fun"</span><span class="p">,</span> <span class="mf">0.45</span>
<span class="n">pie_data</span><span class="p">.</span><span class="nf">insert_value</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"Useful"</span><span class="p">,</span> <span class="mf">0.25</span>
<span class="n">pie_data</span><span class="p">.</span><span class="nf">insert_value</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">"Cool"</span><span class="p">,</span> <span class="mf">0.15</span>
<span class="n">pie_data</span><span class="p">.</span><span class="nf">insert_value</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">"Enterprisey"</span><span class="p">,</span> <span class="mf">0.10</span>
<span class="n">pie_data</span><span class="p">.</span><span class="nf">insert_value</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">"Exciting"</span><span class="p">,</span> <span class="mf">0.5</span>

<span class="c1"># Create a pie chart with default settings</span>
<span class="n">pie_chart</span> <span class="o">=</span> <span class="n">org</span><span class="p">.</span><span class="nf">jfree</span><span class="p">.</span><span class="nf">chart</span><span class="o">.</span><span class="no">ChartFactory</span><span class="p">.</span><span class="nf">create_pie_chart</span> <span class="s2">"Why JRuby?"</span><span class="p">,</span> <span class="n">pie_data</span>

<span class="c1"># Anti-alias the chart to look a bit cleaner</span>
<span class="n">pie_chart</span><span class="p">.</span><span class="nf">anti_alias</span> <span class="o">=</span> <span class="kp">true</span>

<span class="c1"># Access the actual PiePlot to tweak additional settings</span>
<span class="n">pie_plot</span> <span class="o">=</span> <span class="n">pie_chart</span><span class="p">.</span><span class="nf">plot</span>
<span class="n">pie_plot</span><span class="p">.</span><span class="nf">set_explode_percent</span> <span class="s2">"Fun"</span><span class="p">,</span> <span class="mf">0.20</span>

<span class="c1"># Create a buffered image in memory at 500x500</span>
<span class="n">pie_image</span> <span class="o">=</span> <span class="n">pie_chart</span><span class="p">.</span><span class="nf">create_buffered_image</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">500</span>

<span class="c1"># Write the image as a GIF to a file</span>

<span class="n">pie_file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"piechart.gif"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="n">javax</span><span class="p">.</span><span class="nf">imageio</span><span class="o">.</span><span class="no">ImageIO</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">pie_image</span><span class="p">,</span> <span class="s2">"gif"</span><span class="p">,</span> <span class="n">pie_file</span><span class="p">.</span><span class="nf">to_outputstream</span><span class="p">)</span>
</code></pre></div></div>

<p>This example takes advantage of a few customizations provided by JFreeChart:</p>

<ul>
  <li>The edges of the pie are set to anti-alias for a cleaner look (<code class="language-plaintext highlighter-rouge">pie_chart.anti_alias = true</code>, which calls <a href="https://www.jfree.org/jfreechart/javadoc/org/jfree/chart/JFreeChart.html#setAntiAlias(boolean)">setAntiAlias</a>).</li>
  <li>We access the actual <a href="https://www.jfree.org/jfreechart/javadoc/org/jfree/chart/plot/PiePlot.html">PiePlot</a> object to <a href="https://www.jfree.org/jfreechart/javadoc/org/jfree/chart/plot/PiePlot.html#setExplodePercent(K,double)">“explode”</a> one of the elements out of the pie.</li>
  <li>Instead of a PNG, we output a GIF, just because. The standard <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/javax/imageio/package-summary.html">Java ImageIO support</a> can handle BMP, GIF, JPEG, PNG, TIFF, and WBMP, and there’s third-party support for everything else.</li>
</ul>

<p>And here’s the resulting pie chart:</p>

<p><img src="/images/piechart.gif" alt="A pie chart generated by JRuby and JFreeChart" /></p>

<p>It’s as easy as pie!</p>

<h1 id="your-turn">Your Turn</h1>

<p>In this post, you learned the following:</p>

<ul>
  <li>Basics of Java integration in JRuby</li>
  <li>How to fetch and load Java libraries from Maven</li>
  <li>A few simple ways to create charts with JFreeChart</li>
</ul>

<p>We didn’t have to write a single line of Java code, and we didn’t have to call out to any nasty C, Python, or JavaScript libraries. The code you see and the libraries we loaded all run in the same JVM process alongside your Ruby code, and can be easily deployed to any system with a JDK. It’s really that simple!</p>

<p>JFreeChart is just one charting library out of many in the Java ecosystem, and there’s thousands of other useful libraries you can start using with JRuby today. Need to generate PDFs or Office documents? Try <a href="https://github.com/LibrePDF/OpenPDF">OpenPDF</a> or <a href="https://poi.apache.org/">Apache Poi</a>. Need to integrate with unusual databases? <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/package-summary.html">JDBC</a> has you covered with a standard API. Want to deploy a single binary for your entire application? JRuby’s <a href="https://github.com/jruby/warbler">Warbler</a> project allows you to bundle everything up as a single jar file.</p>

<p>Ruby faces many challenges these days, and we’re solving them one at a time with JRuby and the JVM. I hope you will experiment with JRuby yourself and create something beautiful!</p>

<h2 id="join-the-discussion-on-reddit"><a href="https://www.reddit.com/r/ruby/comments/1kbqh5l/creating_beautiful_charts_with_jruby_and/">Join the discussion on Reddit!</a></h2>

<h1 id="jruby-support-and-sponsorship"><em>JRuby Support and Sponsorship</em></h1>

<h2 id="this-is-a-call-to-action"><em>This is a call to action!</em></h2>

<p><em>JRuby development is funded entirely through your generous sponsorships and the sale of commercial support contracts for JRuby developers and enterprises around the world. If you find my work exciting or believe it is important your company or your projects, please consider partnering with me to keep JRuby strong and moving forward!</em></p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[I recently returned from RubyKaigi where I had the opportunity to sit down with members of the Japanese Ruby community and show them a little bit of JRuby. One of the items that came up a few times was the difficulty of utilizing external libraries from Ruby: if it’s a C library, typically you have to either write a C extension or do the extra work of writing up an FFI binding.]]></summary></entry><entry><title type="html">JRuby 10, Part 1: What’s New</title><link href="https://headius.github.io/2025/04/jruby-10-part-one-whats-new.html" rel="alternate" type="text/html" title="JRuby 10, Part 1: What’s New" /><published>2025-04-09T00:00:00+00:00</published><updated>2025-04-09T00:00:00+00:00</updated><id>https://headius.github.io/2025/04/jruby-10-part-one-whats-new</id><content type="html" xml:base="https://headius.github.io/2025/04/jruby-10-part-one-whats-new.html"><![CDATA[<p>I am very excited to introduce you to JRuby 10, our biggest leap forward since “JRuby 9000” was released almost a decade ago.</p>

<p>With up-to-date Ruby compatibility, support for modern JVM features, and a big cleanup of internal code and external APIs, we believe this is our most important release ever. This article will provide a first look at these improvements and help you get started on your JRuby 10 journey.</p>

<h1 id="moving-forward">Moving Forward</h1>

<p>With such a long time since our last major release, we decided JRuby 10 had to make some big moves. As a result, this is the most up-to-date and powerful JRuby release we’ve ever put together. Here’s a few of the major upgrades you’ll see when you move to JRuby 10.</p>

<h3 id="compatibility-jump-to-ruby-34">Compatibility jump to Ruby 3.4</h3>

<p>It’s been over three years since we released <a href="https://www.jruby.org/2022/11/23/jruby-9-4-0-0.html">JRuby 9.4</a> with Ruby 3.1 compatibility, so naturally a version update was in order. Last year we decided it made the most sense for us to target Ruby 3.4, since it would be released late in 2024 around the time we hoped to wrap up JRuby 10.</p>

<p>That meant implementing <a href="https://github.com/jruby/jruby/issues/7517">Ruby 3.2</a>, <a href="https://github.com/jruby/jruby/issues/8029">3.3</a>, <em>and</em> <a href="https://github.com/jruby/jruby/issues/8395">3.4</a> features, and getting literally thousands of new tests and specs passing.</p>

<p><em>CRuby core class test results</em></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>JRuby 9.4 running Ruby 3.1 tests:

4888 tests, 1859373 assertions, 0 failures, 0 errors, 22 skips

JRuby 10 running Ruby 3.4 tests:

5461 tests, 1903359 assertions, 0 failures, 0 errors, 43 skips
</code></pre></div></div>

<p>Over the past month, we merged our JRuby 10 development branch back to main, and just this week we finally went <a href="https://github.com/jruby/jruby/actions">“green” in CI</a> after hundreds of hours of work. We’re confident this is the most compatible a JRuby “dot zero” release has ever been, and being current on Ruby compatibility means we’ve got more time to work on aggressive optimization plans.</p>

<h3 id="java-up-to-21">Java up to 21</h3>

<p>Like many projects in the Java world, we chose to maintain support for Java 8 for over a decade after it was released, due to challenging issues involving Java 9+ and the new restrictions of the Java Platform Module System. Even though we made sure JRuby supported the latest Java releases, we could only depend upon Java 8-level features. Meanwhile the JDK added powerful enhancements like native function calls (<a href="https://openjdk.org/projects/panama/">Project Panama</a>) and lightweight threads (<a href="https://openjdk.org/projects/loom/">Project Loom</a>).</p>

<p>For JRuby 10, we’re finally updating our minimum Java version… to the most recent “long term support” version 21. Many of the JRuby features described in this post are possible because of that move.</p>

<p>We’ve already started to integrate the features that <a href="https://openjdk.org/projects/jdk/21/">Java 21</a> provides, and we’re looking forward to bringing ten years of JVM enhancements to Ruby users.</p>

<h3 id="full-optimization-by-default">Full optimization by default</h3>

<p>Starting with Java 7, JRuby has supported optimizing Ruby code using a feature called “<a href="https://wiki.openjdk.org/display/HotSpot/Method+handles+and+invokedynamic">invokedynamic</a>”, which allows us to teach the JVM how Ruby code works. “Indy” is absolutely critical for Ruby performance on JRuby, but it has also taken time to evolve at the JVM level. Because of the extra startup and warmup time required to use indy on older JVMs, JRuby ran by default in a “middle tier” of optimization, using indy only for simple Ruby operations and using slower inline caching techniques for heavier ones. Users had to enable indy with the JRuby flag <code class="language-plaintext highlighter-rouge">-Xcompile.invokedyamic</code>.</p>

<p>But no longer!</p>

<p>JRuby 10 runs with <a href="https://github.com/jruby/jruby/pull/8450">full invokedynamic optimization</a> by default. That means you’ll get the best available performance for your JRuby scripts and applications without passing any additional flags.</p>

<p><em>JRuby default red/black performance, 9.4 vs 10</em></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby9.4 -v bench/bench_red_black.rb 5
jruby 9.4.12.0 (3.1.4) 2025-02-11 f4ab75096a OpenJDK 64-Bit Server VM 21.0.5+11-LTS on 21.0.5+11-LTS +jit [arm64-darwin]
1.0899240000000001
0.632956
0.604672
0.612468
0.5976049999999999
$ jruby -v bench/bench_red_black.rb 5
jruby 10.0.0.0-SNAPSHOT (3.4.2) 2025-04-09 e2909f9baf OpenJDK 64-Bit Server VM 21.0.5+11-LTS on 21.0.5+11-LTS +indy +jit [arm64-darwin]
1.651375
0.46118200000000004
0.363622
0.23990799999999998
0.177958
</code></pre></div></div>

<p>For development and testing environments, where aggressive optimizations are usually not helpful and just slow down command-line use, we provide other options…</p>

<h3 id="startup-time-improvements">Startup time improvements</h3>

<p>The number one complaint from JRuby users has always been startup time (and to a lesser extent, warmup time of large apps). With the leap to Java 21, we’ve started to leverage a few new JVM features to get JRuby apps starting up more quickly:</p>

<ul>
  <li>OpenJDK’s “<a href="https://openjdk.org/jeps/310">Application Class Data Store</a>” (AppCDS) allows pre-caching code and metadata during startup to reduce the cost of future commands. JRuby’s <a href="https://github.com/jruby/jruby/blob/master/bin/jruby.sh">main executable</a> will automatically use the right AppCDS flags on Java 21+ to optimize and cache as much as possible. This has halved the startup time of a typical JRuby command, and there’s a lot more we can do to utilize this feature.</li>
  <li><a href="https://openjdk.org/projects/crac/">Project CRaC</a> (Coordinated Restore at Checkpoint) is an experimental JVM feature that allows users to “checkpoint” a running process and launch multiple future processes by restoring that checkpoint. There’s limitations, such as only being able to restore a single process at a time, but when CRaC works for you it can reduce startup time of even large apps to just a few milliseconds. The JRuby launcher supports CRaC with a few flags described in my first “<a href="https://blog.headius.com/2024/09/jruby-on-crac-part-1-lets-get-cracking.html">JRuby on CRaC</a>” blog post.</li>
  <li><a href="https://openjdk.org/projects/leyden/">Project Leyden</a> is the next-generation “AppCDS”, also storing data like JIT-compiled native code and optimization profiles from previous runs. The goal of Leyden is to eventually save off everything needed to start right up with optimized code, skipping the slow early stages of execution. The JRuby Team will incorporate Leyden flags into our launcher as they become available (with preview support already in Java 24)… and JRuby users will just have to upgrade their JDK to take advantage.</li>
</ul>

<p>These features combined with our reduced-overhead <code class="language-plaintext highlighter-rouge">--dev</code> flag mean JRuby starts up faster than ever before… fast enough to take most of the pain out of command-line development.</p>

<p><em>JRuby “hello world” startup time, 9.4 vs 10</em></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[] jruby $ time jruby9.4 --dev -e 'puts "Hello, world!"'
Hello, world!
        0.99 real         1.13 user         0.10 sys
[] jruby $ time jruby --dev -e 'puts "Hello, world!"'
Hello, world!
        0.81 real         0.91 user         0.08 sys
</code></pre></div></div>

<h1 id="getting-started">Getting Started</h1>

<p>Trying JRuby has never been easier! Here’s a quick walkthrough on getting JRuby into your development toolbox.</p>

<h3 id="install-a-jdk">Install a JDK</h3>

<p>The only requirement for JRuby 10 is a Java 21 or higher JDK installation. I personally like the <a href="https://www.azul.com/downloads/?package=jdk#zulu">OpenJDK “Zulu” builds from Azul</a>, but there’s excellent and well-supported free binaries from the <a href="https://adoptium.net/temurin/releases/">Eclipse project</a>, <a href="https://docs.aws.amazon.com/corretto/latest/corretto-21-ug/downloads-list.html">Amazon</a>, <a href="https://www.microsoft.com/openjdk">Microsoft</a>, and <a href="https://www.oracle.com/java/technologies/downloads/">Oracle</a>.</p>

<p>Install the JDK in whatever way is most appropriate for your platform… usually that will mean a system-level package on Linux/BSD or an installer on MacOS and Windows. Then just point your <code class="language-plaintext highlighter-rouge">JAVA_HOME</code> environment variable at the JDK and JRuby’s launcher will figure out the rest.</p>

<h3 id="install-jruby">Install JRuby</h3>

<p>Most Rubyists will be familiar with Ruby managers and switchers like <a href="https://github.com/rbenv/rbenv">rbenv</a>, <a href="https://rvm.io/">rvm</a>, or <a href="https://github.com/postmodern/chruby">chruby</a>. JRuby 10 preview snapshots can be usually be installed as “jruby-head” or “jruby-dev”, and after our release as “jruby-10” or just “jruby”.</p>

<p>JRuby is also a JVM-based project (“write once, run anywhere”, remember?), so <a href="https://www.jruby.org/">installing it yourself</a> is as easy as unpacking a tarball or zip and putting the <code class="language-plaintext highlighter-rouge">bin</code> dir in your <code class="language-plaintext highlighter-rouge">PATH</code>. There’s no build step and no build tools needed to start using JRuby today.</p>

<p>Users of <code class="language-plaintext highlighter-rouge">chruby</code> can unpack this tarball into <code class="language-plaintext highlighter-rouge">~/.rubies</code> since it does not support “head” installs.</p>

<h3 id="try-it-out">Try it out</h3>

<p>JRuby supports the standard <code class="language-plaintext highlighter-rouge">irb</code> REPL, as well as other modern alternatives like <code class="language-plaintext highlighter-rouge">pry</code>. Once you have <code class="language-plaintext highlighter-rouge">jruby -v</code> working, you can <code class="language-plaintext highlighter-rouge">gem install</code> your favorite tools and test them out on JRuby. It’s really that easy.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ irb
irb(main):001&gt; JRUBY_VERSION
=&gt; "10.0.0.0-SNAPSHOT"
irb(main):002&gt; RUBY_VERSION
=&gt; "3.4.2"
irb(main):003&gt; java.lang.System.get_property("java.version")
=&gt; "21.0.5"
</code></pre></div></div>

<h1 id="riding-the-rails">Riding the Rails</h1>

<p>Ruby without Rails would be like… well I guess it would still be Ruby, but there’s no doubt Rails is still the most popular use case for Ruby. JRuby has supported Rails since the 1.x era, and we’ve worked with the Rails and JRuby community to keep up with new releases and features. JRuby represents the best way to achieve single-process multicore scaling of Rails applications, and thousands of Rails users around the world rely on it to better utilize precious computing resources.</p>

<p>We’ll publish a post soon that describes how to get started with JRuby on Rails, all the way from generating an app to deploying on Puma or inside a Java application server.</p>

<h3 id="rails-compatibility">Rails compatibility</h3>

<p>Keeping up with Rails is no small feat, and with only a handful of contributors we have tended to lag a bit behind. JRuby currently has database support for ActiveRecord up to 7.1, and work is ongoing to support Rails 8. If you have an interest in helping the JRuby team update our ActiveRecord adapter (<a href="https://github.com/jruby/activerecord-jdbc-adapter">ActiveRecord-JDBC</a>, which uses the pure-JVM Java DataBase Connectivity API), please let us know! We’re hoping to have full support for Rails 8 by this summer.</p>

<h3 id="generating-an-app">Generating an app</h3>

<p>The typical Rails commands should all work on JRuby exactly the same as on standard CRuby, and with our startup time improvements and the –dev flag, we’re able to run those commands faster than any past JRuby version.</p>

<h3 id="config-changes">Config changes</h3>

<p>Only minor configuration changes are required to run a Rails app on JRuby:</p>

<ul>
  <li>Switch to the appropriate ActiveRecord-JDBC adapter for your database, in <code class="language-plaintext highlighter-rouge">database.yml</code> and in your <code class="language-plaintext highlighter-rouge">Gemfile</code>.</li>
  <li>Configure the Puma server for a number of threads based on the CPU cores available, using “2n+1” as a good rule of thumb.</li>
  <li>Make sure there’s enough database connections in the pool for the number of Puma threads you configure.</li>
</ul>

<p>Once you’ve generated a new app or made config changes to your existing app, just <code class="language-plaintext highlighter-rouge">bundle</code> and fire it up! JRuby is a true Ruby implementation, and we work very hard to ensure Ruby libraries and applications like Rails work the same as on CRuby.</p>

<h1 id="integrating-with-jvm-languages">Integrating with JVM Languages</h1>

<p>Half the fun of JRuby comes from taking advantage of the Java platform. Here’s just a few examples of things you can’t do as easily or quickly on any other Ruby implementation.</p>

<h3 id="ruby-on-the-jvm">Ruby on the JVM</h3>

<p>We’ve always had a goal of keeping JRuby a standard “JVM language”, runnable on any Java build compatible with our minimum requirement. This means you can deploy JRuby on Linux, MacOS, and Windows, of course, but also unusual and exotic platforms like the BSDs, Solaris, and more. We also can run on any hardware supported by the JVM, which basically means any system with enough memory and at least 32 bit registers is fair game.</p>

<p>This also means JRuby can be deployed anywhere Java applications can be deployed, alongside enterprise Java apps using Spring or Jakarta EE. Ruby and Rails developers can expand their target market to any shop that hosts JVM-based apps… which dwarfs the number of shops that would be comfortable installing the libraries and development tools necessary to run CRuby. JRuby brings the Ruby world into the enterprise, and brings enterprise opportunities to every Rubyist.</p>

<p><em>JRuby versus Ruby running threaded “tarai” benchmark</em></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jruby bench_ractors_tarai.rb 
                      user     system      total        real
serial           21.580000   0.070000  21.650000 ( 21.395228)
threaded         32.690000   0.100000  32.790000 (  4.260730)

$ ruby3.4 --yjit bench_ractors_tarai.rb
                      user     system      total        real
serial           25.037682   0.060370  25.098052 ( 25.257134)
threaded         24.998077   0.055508  25.053585 ( 25.058869)
</code></pre></div></div>

<h3 id="calling-into-java">Calling into Java</h3>

<p>Part of running on the JVM is being able to integrate other languages and their libraries into your JRuby apps. With JRuby, you can call Java, <a href="https://www.scala-lang.org/">Scala</a>, <a href="https://clojure.org/">Clojure</a>, <a href="https://kotlinlang.org/">Kotlin</a>, and any other JVM language from Ruby with ease. Imagine bring the entire world of JVM languages and libraries to your Ruby app… what could you do when that kind of power?</p>

<p><em>Using Java’s Swing GUI API from Ruby</em></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">java_import</span> <span class="n">javax</span><span class="p">.</span><span class="nf">swing</span><span class="o">.</span><span class="no">JFrame</span>

<span class="n">frame</span> <span class="o">=</span> <span class="no">JFrame</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Hello Swing"</span><span class="p">)</span>
<span class="n">button</span> <span class="o">=</span> <span class="n">javax</span><span class="p">.</span><span class="nf">swing</span><span class="o">.</span><span class="no">JButton</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Klick Me!"</span><span class="p">)</span>
<span class="n">button</span><span class="p">.</span><span class="nf">add_action_listener</span> <span class="k">do</span> <span class="o">|</span><span class="n">evt</span><span class="o">|</span>
  <span class="n">javax</span><span class="p">.</span><span class="nf">swing</span><span class="o">.</span><span class="no">JOptionPane</span><span class="p">.</span><span class="nf">showMessageDialog</span><span class="p">(</span><span class="kp">nil</span><span class="p">,</span> <span class="o">&lt;&lt;~</span><span class="no">EOS</span><span class="p">)</span><span class="sh">
    &lt;html&gt;Hello from &lt;b&gt;&lt;u&gt;JRuby&lt;/u&gt;&lt;/b&gt;.&lt;br&gt;
    Button '</span><span class="si">#{</span><span class="n">evt</span><span class="p">.</span><span class="nf">getActionCommand</span><span class="p">()</span><span class="si">}</span><span class="sh">' clicked.
</span><span class="no">  EOS</span>
<span class="k">end</span>

<span class="c1"># Add the button to the frame</span>
<span class="n">frame</span><span class="p">.</span><span class="nf">get_content_pane</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">button</span><span class="p">)</span>

<span class="c1"># Show frame</span>
<span class="n">frame</span><span class="p">.</span><span class="nf">set_default_close_operation</span><span class="p">(</span><span class="no">JFrame</span><span class="o">::</span><span class="no">EXIT_ON_CLOSE</span><span class="p">)</span>
<span class="n">frame</span><span class="p">.</span><span class="nf">pack</span>
<span class="n">frame</span><span class="p">.</span><span class="nf">visible</span> <span class="o">=</span> <span class="kp">true</span>
</code></pre></div></div>

<h3 id="using-jvm-libraries">Using JVM libraries</h3>

<p>Of course in order to use more than just the standard JDK libraries, you need to be able to download and load them into your app. JRuby provides tools like <a href="https://github.com/jruby/jar-dependencies">jar-dependencies</a> to let your JRuby gems use libraries published to Maven Central, <a href="https://github.com/jruby/warbler">Warbler</a> to bundle your app and libraries into a single executable jar or war file (“Web ARchive”), and an enhanced <code class="language-plaintext highlighter-rouge">require</code> that loads Java JAR files right into your app. It’s the easiest and most fun way to explore all that the JVM has to offer.</p>

<p>Want to build a cross-platform desktop UI? The JVM has a half-dozen different frameworks for building GUI applications, and with JRuby-supported libraries like <a href="https://github.com/AndyObtiva/glimmer">Glimmer</a> and <a href="https://github.com/jruby/jrubyfx">JRubyFX</a>, you don’t have to give up Ruby to get there.</p>

<p><em>Hello World in Glimmer</em></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kp">include</span> <span class="no">Glimmer</span>

<span class="n">shell</span> <span class="p">{</span>
  <span class="n">text</span> <span class="s2">"Glimmer"</span>

  <span class="n">label</span> <span class="p">{</span>
    <span class="n">text</span> <span class="s2">"Hello, World!"</span>
  <span class="p">}</span>
<span class="p">}.</span><span class="nf">open</span>
</code></pre></div></div>
<p><img src="/images/glimmer-hello-world.png" alt="Hello World in Glimmer" /></p>

<p>Interested in building Android apps for mobile or embedded applications? JRuby provides the <a href="http://ruboto.org/">Ruboto</a> framework for building Android apps in Ruby without having to write a single line of Java or Kotlin.</p>

<p>This also puts you on the leading edge of emerging technologies. As the JVM platform evolves and new features like Leyden, CRaC, <a href="https://openjdk.org/jeps/448">SIMD vector operations</a>, <a href="https://openjdk.org/projects/babylon/">GPU integration</a> and native function support in Panama move from experimental to standard, your apps can start using them with a simple JDK upgrade.</p>

<p>You’ve even got a leg up on adding AI capabilities to your app; Ruby APIs for ChatGPT, Claude, Copilot and others are just starting to appear, but there’s dozens of existing JVM libraries for these LLMs and other bleeding edge AI technologies. You can start using AI in your Ruby apps today with JRuby!</p>

<h1 id="whats-next">What’s Next?</h1>

<p>Over the coming weeks, I’ll publish more detailed posts on each of these areas to help you get started as a new JRuby user or to upgrade your existing JRuby applications. We are very proud of the work we’ve done to bring JRuby 10 to life, and I guarantee every Ruby shop can be faster and more scalable by taking advantage of JRuby and the JVM.</p>

<p>Ruby’s future depends on projects like JRuby and creative developers like you. Let’s show them what we can do!</p>

<h2 id="join-the-discussion-on-reddit"><a href="https://www.reddit.com/r/ruby/comments/1jv91vy/jruby_10_part_1_whats_new/">Join the discussion on Reddit!</a></h2>

<h1 id="jruby-support-and-sponsorship"><em>JRuby Support and Sponsorship</em></h1>

<p><em>This is a call to action!</em></p>

<p><em>JRuby development is funded entirely through your generous sponsorships and the sale of commercial support contracts for JRuby developers and enterprises around the world. If you find my work exciting or believe it is important your company or your projects, please consider partnering with me to keep JRuby strong and moving forward!</em></p>

<p><a href="https://github.com/sponsors/headius">Sponsor Charles Oliver Nutter on GitHub!</a></p>

<p><a href="https://www.headius.com/jruby-support">Sign up for expert JRuby support from Headius Enterprises!</a></p>]]></content><author><name>Charles Oliver Nutter</name></author><summary type="html"><![CDATA[I am very excited to introduce you to JRuby 10, our biggest leap forward since “JRuby 9000” was released almost a decade ago.]]></summary></entry></feed>