Apache Flink クラスローダーについて
Apache Flink クラスローダーについて
▫️概要
Apache Flink のクラスローディングは、ジョブごとに独立したクラスローダーを生成し、
ユーザコードと Flink 本体を安全に分離する仕組みです。
これにより、異なるジョブが異なる依存ライブラリを使っても衝突を防げます。
本記事では、Java の基本的なクラスローダー構造から始め、
Flink のクラスローダー設計、設定方法、メモリリーク対策までをわかりやすく解説します。
▫️前提
1. そもそも Java プログラムはどう動いているのか?
Java プログラムは次の流れで動作します。
-
.javaファイルをコンパイルして.class(バイトコード)を生成 - JVM(Java 仮想マシン)が
.classを読み込み実行 - 実行時に「必要なクラス」を動的にロード
JVM は 実行前にすべてのクラスを知っているわけではなく、
必要になったタイミングでクラスを探してロードします。
この「クラスをどこから探すか・どう読み込むか」を担うのが クラスローダー(ClassLoader) です。
2. クラスローダーとは何者か?
クラスローダーの役割を一言で言うと:
「クラス名」から「クラスの実体(.class)」を見つけて JVM に渡す係
Java の世界では:
- コードでは
com.example.Fooのようなクラス名を使う - 実際のクラスは
.classファイルや.jarに格納 - それを見つけて JVM に登録するのがクラスローダーです
3. クラスローダーの「親子関係」
クラスローダーは 階層構造(ツリー構造) を持ちます。
基本ルールは 親から順番にクラスを探す(Parent-First) です。
主な階層
| 層 | 名称 | 読み込む対象 |
|---|---|---|
| 最上位 | Bootstrap ClassLoader | Java 標準クラス(java.lang.String など) |
| 中間層 | Platform / Application ClassLoader | JDKの追加APIやアプリ全体 |
| 最下層 | Custom / Framework ClassLoader | フレームワークやユーザジョブ用クラス |
上に行くほど JVM に近く、
下に行くほどユーザコードに近い層になります。
▫️Apache Flink におけるクラスローダー
1. Flink のクラスローディング設計
Flink では、ジョブごとに独立した UserCodeClassLoader を作成します。
これにより、複数ジョブが異なる依存関係(例:Guava のバージョン違いなど)を持っていても
互いに干渉しないように設計されています。
2. クラスローダーの階層構造
Flink のクラスローダーは以下の2層構造が中心です。
| 階層 | 内容 | 例 |
|---|---|---|
| Flink Core ClassLoader(親層) | Flink 本体と共通ライブラリをロード |
flink-core, flink-runtime, guava
|
| UserCodeClassLoader(子層) | ジョブ固有の依存ライブラリ・ユーザコードをロード |
myjob.jar, mysql-connector-j.jar
|
3. クラスローディング順序設定
flink-conf.yaml で以下の設定が可能です:
classloader.resolve-order: child-first
| モード | 探索順序 | 特徴 | 主な用途 |
|---|---|---|---|
| Parent-First | 親 → 子 | Flink 本体を優先。安定性重視。 | 標準API利用ジョブ |
| Child-First | 子 → 親 | ユーザ依存を優先。柔軟性重視。 | JDBC, Guava, Jackson など外部依存あり |
特に 外部ライブラリが Flink 本体とバージョン競合する場合、
child-first 設定が推奨されます。
4. メモリリーク(ClassLoader Leak)と対策
Flink はジョブごとに新しい UserCodeClassLoader を作成し、
ジョブ終了時に破棄します。
ただし、参照が残ると GC に回収されず、
メモリリーク(ClassLoader Leak) が発生することがあります。
主な原因と対処
| 原因 | 説明 | 対応策 |
|---|---|---|
| static 参照 | static フィールドにユーザクラスを保持 | static を避け、依存注入(DI)を使う |
| ThreadLocal 残留 | ThreadLocal にユーザクラスを残す |
ThreadLocal.remove() を明示的に呼ぶ |
| DriverManager 登録残り | JDBC ドライバが登録解除されない | ジョブ終了時に DriverManager.deregisterDriver()
|
| child-first による親参照 | 親ローダーが子クラスを参照 |
parent-first に戻す or Shade Plugin で名前空間を隔離 |
5. まとめ
| 観点 | parent-first | child-first |
|---|---|---|
| 探索順序 | 親 → 子 | 子 → 親 |
| 安定性 | 高い | 低い(衝突リスクあり) |
| 柔軟性 | 低い | 高い(外部依存優先可) |
| 主な用途 | 長期ジョブ、標準API | 外部依存利用ジョブ(JDBC等) |
| メモリリークリスク | 小 | 大(親参照によるリーク) |
▫️結論
Flink のクラスローディングは、
「安定性(parent-first)」と「柔軟性(child-first)」のバランス設計 が鍵です。
基本は安定性を重視して parent-first を使い、
外部依存を含む場合のみ child-first に切り替えるのが実践的です。
また、ジョブ終了後の クラスローダーリーク防止(static・ThreadLocal の解放) は
運用上の重要なポイントです。
✅ 安定稼働を重視:
parent-first
✅ 外部ライブラリ利用時:child-first
✅ メモリリーク防止:参照を適切に解放
📚 参考文献・引用元
- Flink GitHub – flink-runtime module
- Oracle Java SE Documentation – ClassLoader
- Baeldung – Java ClassLoader Basics
Discussion