📓

なぜ、Javaより遅いPythonが組込みシステムで使えるのにJavaは無理なのか?

に公開

はじめに

一般的なプログラミング言語の性能比較において「JavaはPythonより高速である」というのはほぼ常識的な認識です。実際、同じ処理を実行すれば、JIT(Just-In-Time)コンパイルを備えたJava仮想マシン(JVM)の方が、インタプリタ方式のPythonより何倍も速い結果を示します。
しかし現実の組込み分野では、Python(特にMicroPythonやCircuitPython)がマイコン上で活用されているのに対して、Javaはほとんど使われていないという逆転現象が起きています。

この矛盾を単純に「速度」の問題として理解しようとすると本質を見失います。両者の差は「速いか遅いか」ではなく、**「いつ動くかが決まっているかどうか」**という、時間制御の思想の違いにあります。本稿では、言語処理系・ガーベジコレクション(GC)・実行モデル・リアルタイム性といった観点から、この根本的な差を掘り下げます。

Javaの設計思想とその強み

Javaは1995年に登場した、プラットフォーム非依存型のオブジェクト指向言語です。その最大の特徴は、仮想マシン上で動作することによる移植性と、自動メモリ管理による安全性 です。JVMはコードを中間バイトコードに変換し、実行時にJITコンパイルによってネイティブコードを生成します。この仕組みにより、Javaは「書いたコードがどのOS・CPUでも同様に動作する」という汎用性を手に入れました。

また、メモリ管理も自動化されています。CやC++のようにfree()deleteを手動で呼ぶ必要がなく、ガーベジコレクタ(GC) が不要になったオブジェクトを自動的に回収します。この仕組みにより、メモリリークやダングリングポインタのリスクを劇的に減らすことに成功しました。

このように、Javaは「ヒューマンエラーを減らし、アプリケーション全体の信頼性を上げる」という目的で設計された高水準言語です。つまり、Javaが想定していた環境はPCサーバーや業務システムなど、一定のリソースとCPU時間を前提に動作する非リアルタイム環境です。

ガーベジコレクションという“時間を止める技術”

Javaを組込みシステムに導入する際の最大の壁が、GC(ガーベジコレクション)によるstop-the-world問題です。

JVMは主に「世代別マーク&スイープGC(Generational Mark & Sweep GC)」を採用しています。この方式では、オブジェクトの寿命を統計的に分類し、若いオブジェクト(新生代)は頻繁に回収し、長生きするオブジェクト(老年世代)は時々まとめて回収します。

これはスループット最適化には非常に効果的ですが、致命的な問題があります。GC中はプログラム全体が一時停止する(stop-the-world) のです。この停止時間はヒープサイズに比例して伸び、場合によっては数百ミリ秒、場合によっては数秒単位に及びます。

この現象をハードウェアの視点で言い換えると、世界の時間を止める=CPU全コアへのNMI(Non Maskable Interrupt)発行に相当します。NMIは通常の割り込み(IRQ)とは異なり、マスクできず、すべての処理を強制的に中断させる特権割り込みです。つまり、stop-the-worldとはソフトウェア層における「全コア強制停止命令」に他なりません。

この挙動を直感的に理解する比喩として最も適しているのが、荒木飛呂彦の漫画『ジョジョの奇妙な冒険』に登場するディオ・ブランドーのスタンド「ザ・ワールド」 です。「ザ・ワールド」は時間を止める能力を持ち、敵も味方も動けない中でディオだけが自由に行動します。これは、GCが発動したときのJVMの状態そのものです。

アプリケーション(=人間たち)は完全に静止し、ガーベジコレクタ(=ディオ)がメモリ空間を縦横無尽に歩き回り、不要なオブジェクトを掃除していくのです。
GCは世界を秩序立てるが、そのためには 「NMI級の一時停止」 という強権を行使する。これがリアルタイム分野における最大の問題なのです。

リアルタイム制御の現場では、NMIですら「最後の安全網」であり、通常運転中にNMIを多用する設計はありえません。それを平常時に繰り返すのが、JavaのGCというわけです。

マーク・アンド・スイープと「押し出しファイリング」

マーク・アンド・スイープGCを理解する上で興味深いのが、経済学者・野口悠紀雄の著書『「超」整理法』(中公新書, 1993年)に登場する 「押し出しファイリング」 との類似性です。

押し出しファイリングとは、書類を分類せず、とにかく新しいものを前に突っ込み、古くなったものは後ろへ押し出されていくという方式です。一定量を超えたら、後ろの方──つまり長い間使われていない書類──をまとめて捨てる。これはまさに、世代別GCが採用しているアルゴリズムと同じ構造です。

新しいオブジェクトは新生代ヒープに置かれ、寿命が短いものは早めに消え、何度も生き残ったものだけが老年世代に“押し出され”ます。そして、老年世代がいっぱいになると、「後ろの方をまとめて掃除」するのです。

つまり、押し出しファイリングとは紙の上で行う人間的ガーベジコレクションです。野口氏の狙いは、情報の分類という“思考コスト”を最小化し、時間を軸に整理を自動化することにありました。結果的に、彼が考案した整理法は、局所性の原理を利用したメモリ管理法と構造的に一致しているのです。

ただし、違いもあります。人間の押し出しファイリングでは“いつ掃除するか”を自分で決められますが、JavaのGCでは“VMが勝手に掃除を始める”ため、人間が手を止めてしまう。ここでも「時間を誰が支配しているか」という問題が浮き彫りになります。押し出しファイリングが「人間主導の整理術」であるのに対し、GCは「システム主導の停止と再編」なのです。

GCとキャッシュメモリ構造の驚くべき類似

マーク・アンド・スイープGCの構造は、CPUキャッシュ階層(L1〜L4キャッシュ) にも似ています。CPUは、アクセス頻度の高いデータをL1キャッシュに、やや古いものをL2・L3キャッシュに、最終的にDRAM(主記憶)へと押し出していきます。これは「時間的局所性」と「空間的局所性」に基づく設計です。

世代別GCも同様で、新生代はL1、サバイバー領域はL2、老年世代はL3やメインメモリに相当します。つまりGCは 「古いデータを押し出して新しい空間を確保するキャッシュのような存在」 です。

ただし、CPUキャッシュはノンブロッキングで、裏で滑らかに動きます。JavaのGCは一度「時を止め」て整理するため、リアルタイム性を失うのです。これはキャッシュと同じ思想を持ちながら、「非同期更新」と「同期停止整理」という設計思想の差がもたらす明暗と言えます。

GCはウォッチドッグリセットのような“定期検診”

マーク・アンド・スイープGCは、ウォッチドッグタイマ(WDT) にも似ています。ウォッチドッグはCPUが暴走していないかを定期的に確認し、応答がなければシステムを強制リセットします。つまり、「一時停止して全体を確認する」という思想において、GCと本質的に共通しています。

この点で、JavaのGCは常時発動するウォッチドッグリセットといえます。暴走を防ぐという目的自体は安全で合理的ですが、問題はその「頻度」と「制御不能性」です。
リアルタイムシステムでは、ウォッチドッグリセットは最終手段に過ぎません。平常時にそれが発動するような設計は致命的です。

GCも同様に、平常運転の中で全スレッドを止めてメモリを掃除します。つまり、秩序を守るために時間を犠牲にする構造なのです。リアルタイム制御では「止まらないこと」が安全の前提であるため、これは根本的な価値観の衝突です。

Pythonは“遅いけど止まらない”

一方のPython(特にMicroPythonやCircuitPython)は、実行速度ではJavaに大きく劣ります。しかし、そのメモリ管理方式がリファレンスカウント(Reference Counting)+必要に応じた補助的GCであることが、組込みでの採用を可能にしています。

リファレンスカウントとは、各オブジェクトに「何箇所から参照されているか」というカウンタを持たせ、参照がなくなった時点で即座に解放する方式です。この方法の最大の利点は、解放のタイミングが完全に決定論的であることです。

GCのように世界を止める必要がなく、メモリが解放されるのは「参照が消えた瞬間」だけです。このため、リファレンスカウント方式はリアルタイム性を阻害しません。
確かに、循環参照(AがBを参照し、BがAを参照しているなど)のような例外はありますが、これらはアプリケーション設計や補助GCで回避可能です。結果として、Pythonは「遅いが止まらない」 という性質を持ち、組込み制御においてはむしろ好ましい特性を示すのです。

GCとタスクスケジューリングの根本的違い

ここで注目すべきは、JavaのGCがスケジューラの外で動く存在であるのに対し、Pythonのリファレンスカウントはタスク内で完結する操作であることです。

組込みRTOS(Real-Time OS)では、タスク優先度や割り込みハンドラの制御によって処理の順序とタイミングを完全に支配します。しかし、JVMのGCはOSのスケジューリングとは独立しており、VM内部の自己判断でstop-the-worldを発動します。これは、RTOS的には「スケジューラの外から来る外乱」と同じです。

RTOSが全タスクの優先度を完全に制御できるのに、VMがその上で「いきなり全停止」をかける──これはリアルタイム制御の哲学そのものと矛盾します。一方、リファレンスカウント方式は各タスクが自分のメモリを自己管理するため、RTOSのスケジューリングと整合します。

リアルタイムJavaの試みと限界

かつてJavaにもリアルタイム化の試みがありました。代表的なものがRTSJ(Real-Time Specification for Java) です。
RTSJでは、GCをスレッドごとに分離し、“Scoped Memory”を導入してGCを介さずにメモリ確保を行い、優先度逆転を防ぐスケジューリングを導入しました。

しかしこれらの仕組みは、結果的にJavaの「自動メモリ管理」という利点を捨てるものでした。Scoped Memoryを使うということは、結局Cのように手動でメモリを解放することと同義です。それではもはや「Javaらしさ」は残らないのです。

また、RTSJ対応のJVMは一般的なJVMより大きく、複雑で、ライセンスも高価でした。結果として、産業界では一部の学術・航空宇宙・鉄道制御などに限られ、一般的なマイコンレベルの組込みにはほとんど浸透しませんでした。

Pythonが組込みで採用される現実的理由

現代の組込みシステムでは、「すべてをハードリアルタイムで動かす」必要は減りつつあります。
例えば、車載のディスプレイ表示、ログ収集、通信制御などは多少の遅延があっても致命的ではありません。

そうしたソフトリアルタイム領域では、開発効率・保守性・柔軟性が重視されます。
Pythonはその点で、Cとの親和性が高く、スクリプト的に開発できるため、検証スピードが速く、メモリ使用量が小さいMicroPythonが存在します。

つまり、Pythonは「速さ」ではなく「制御の素直さ」と「柔軟性」において組込みに適しているのです。

技術的哲学の差:「世界を止めるVM」と「世界に同調するVM」

JavaとPythonの差を一言で表すなら、以下のように言えます。

  • Java:世界を止めて整合性を取るVM
  • Python:世界に同調して動くVM

Javaはオブジェクト指向の完全性、メモリ整合性、例外安全性といった論理的完全性を優先します。一方、Python(特にMicroPython)は、ヒープ破片や即時解放のような“汚れ”を受け入れてでも、時間の連続性を守る方向に設計されています。

つまり、Javaは「正しさ」を優先し、Pythonは「止まらなさ」を優先しているのです。この思想の差こそ、両者の組込み適性を決定的に分けていると言えます。

リファレンスカウントの原始的単純さと決定論性

PythonやVB、Objective-C、Perlなどの古いスクリプト言語が採用していたリファレンスカウント方式(RC) は、非常に単純です。各オブジェクトが「何箇所から参照されているか」を数え、参照がゼロになった瞬間に即座に解放します。

この方式は、確かに原始的で非効率な面もあります。循環参照を処理できず、参照カウント更新にオーバーヘッドもあります。しかしこの“原始的”こそが組込みにおける最大の強みです。

RCは「いつメモリが解放されるか」が常に明確で、停止が発生しません。割り込み処理やアイドルタスクでも安全に動作し、システム設計者がGCタイミングを意識せずに済みます。これはリアルタイムシステムの基本要件であり、RCは「時間の支配を人間側が保持するGC方式」と言えます。

mrubyのGCはMark & Sweepだが世代別ではない

mrubyはMark & Sweep方式のGCを採用していますが、世代別ではありません
そのため、Javaほどの大規模な“時よ止まれ”は起こりません。mrubyのGCは単一ヒープ上でシンプルにマーク→スイープを行い、オブジェクト数が少ないため停止時間が極めて短いのです。さらに、GCの起動タイミングを開発者が制御できます。リアルタイム処理区間の外でmrb_full_gc()を呼び出すなど、止める瞬間を自分で選べるのです。

JavaのGCが“予告なしのザ・ワールド”なら、mrubyのGCは“手動で呼び出せるザ・ワールド”です。結果として、mrubyは車載機器や産業機器のスクリプト拡張として現実的に運用可能なのです。

組込みが要求する“時間の支配”

組込みシステムが重視するのは決定論的動作(deterministic behavior) です。これは「ある入力に対して、いつ、どのくらいの時間で結果が得られるかが保証される」ことを意味します。

例えば、エンジン制御ユニット(ECU)では、クランク角センサからの信号を受け取ってから点火までの処理時間が常に一定でなければなりません。100回に1回でもGCが走って0.2秒止まれば、エンジンは失火します。

このような分野では、処理の速さよりも「遅延の予測可能性」こそが最優先です。いくらJavaが平均的に速くても、「今この瞬間に止まるかもしれない」という不確定要素を持つ限り、システムとしては信用できません。

まとめ

Javaが組込みに向かないのは「遅いから」ではなく、「世界の時間をNMIで止めるような設計だから」 です。
Pythonやmrubyのような軽量言語は、止めるタイミングを自分で決められるため、リアルタイム制御に適しています。

結論として、「世界を勝手に止めるGC」ではなく、「人間が世界を支配するGC」こそが組込みの現場にふさわしいのです。そして、いかにJavaが賢く速くとも、時間の支配を手放した言語はリアルタイムの世界では生き残れないのです。

Discussion