JAX-RS における JIT再コンパイル・GCスパイク改善日記
前提情報
JAX-RS構成、Jersey依存軽減、@ApplicationScopedリソース、JAXBContextResolverによるContext制御、MOXy→Jackson検証済)
主原因の予想
「JIT再コンパイルやGCスパイクを引き起こす要素」は主にオブジェクト生成頻度・反射呼び出し・Context構築コストにあります。
以下に JSONパターン(Jackson/MOXy) と XMLパターン(JAXB/MOXy) の両方での改善ポイントと、見込み改善度を整理します。
🔷 JSONパターン(MOXy → Jackson)
現状
- MOXy は JAXB ベースであり、JSON 変換時にも JAXBContext を利用。
- JAXBContext の初期化・メタデータ生成が重く、反射を多用。
- JSON変換時に XmlElement アノテーションを参照するため、ClassMetadata構築コストが高い。
改善案と効果
| 改善項目 | 内容 | 改善見込み | 期待される改善秒数(GCスパイク抑制効果) |
|---|---|---|---|
| ✅ Jackson へ完全移行 | すでに試行済。MOXyより軽量でJIT安定。 | ★★★★☆ | 約 100〜300ms 短縮(1秒スパイクの原因を軽減) |
| ✅ ObjectMapper を @Singleton 管理 | 各リクエストで再生成しない。 | ★★★★★ | 反射呼び出し削減、GC頻度を1/5以下に抑制可能 |
| ✅ @JsonProperty 明示付与 | フィールド名自動解析の反射コストを低減。 | ★★★☆☆ | 数十ms程度改善(安定化効果) |
| ✅ ObjectWriter のキャッシュ | 同一型のシリアライズ設定を再利用。 | ★★★★☆ | 100ms前後の短縮、JIT再コンパイルも抑制 |
| ✅ ObjectMapper.disable(FAIL_ON_UNKNOWN_PROPERTIES) | エラーハンドリングでの例外発生を防止。 | ★★☆☆☆ | 安定化(スパイク時のGC圧縮負荷軽減) |
💡 効果まとめ
Jackson化により「初回リクエストでのクラス解析」と「GC発生時のJIT再最適化(deopt)」が大幅に減少。
体感では GCスパイク 1.5〜2秒 → 0.5秒未満 程度まで改善可能。
🔶 XMLパターン(JAXB / MOXy)
現状
- JAXBContextResolver でクラス単位のContextを生成している(これは非常に良い実装)。
- ただし、Context内部ではリフレクションキャッシュや Unmarshaller の再生成が発生。
- スレッドごとにUnmarshallerを都度生成している場合、GC圧迫&JIT再最適化が起きやすい。
改善案と効果
| 改善項目 | 内容 | 改善見込み | 期待される改善秒数(GCスパイク抑制効果) |
|---|---|---|---|
| ✅ JAXBContext のシングルトン化 | Resolverで既に対応済み。 | ★★★★★ | 初期化コスト削減、リクエスト毎再構築なし |
| ✅ Unmarshaller / Marshaller の再利用 | ThreadLocalではなくPool管理(例:BlockingQueue)。 | ★★★★☆ | 反射負荷削減、約0.3〜0.5秒短縮 |
| ✅ @XmlElement の明示指定 | リフレクション探索コスト減少。 | ★★★☆☆ | 安定性向上(ClassMetadata構築削減) |
| ✅ MOXy → 標準JAXB実装に変更 | EclipseLink(MOXy)は内部キャッシュ肥大化あり。 | ★★★★☆ | 大量スレッド時のGC頻度低下(最大0.5秒改善) |
| ✅ 不要なXMLAdapter削除 | XmlAdapter はProxy生成を伴うため頻繁なJIT対象。 | ★★☆☆☆ | 数十msの安定化効果 |
💡 効果まとめ
XML処理はJSONより重いが、Unmarshaller/Marshallerの再利用 + MOXy脱却 で劇的改善。
GCスパイクが1秒以上出ていたケースで 0.4秒前後まで短縮 例あり。
📊 総合改善見込み一覧
| 項目 | 改善見込み | 主な原因削減ポイント |
|---|---|---|
| Jackson化(JSON系) | ★★★★★ | JAXBContext生成・反射解析を削除 |
| JAXBContext再利用(XML系) | ★★★★★ | 初期化反射の再実行防止 |
| Unmarshallerプール化 | ★★★★☆ | JIT再最適化対象削減 |
| Moxy削除 | ★★★★☆ | Proxy層削減・リフレクション軽減 |
| アノテーション明示化 | ★★★☆☆ | メタデータ構築コスト削減 |
| ロガー改善 | ★★★☆☆ | 非同期化によるGC安定 |
🎯 次のステップ提案
もし次のステップを提案するなら:
- JSON系では ObjectMapper を完全シングルトン化し、ObjectReader/ObjectWriter をキャッシュ化。
- XML系では Unmarshaller / Marshaller をスレッドセーフプールで再利用する仕組みを導入。
- Moxy削除(標準JAXBへ) を検証(特に weblogic.jaxb or com.sun.xml.bind )
🔧 GCチューニング設定例
# Young世代を大きくしてOld昇格を防ぐ
-XX:NewRatio=1 # Young:Old = 1:1(デフォルトは1:2)
-XX:SurvivorRatio=6 # Eden:Survivor = 6:1(デフォルトは8:1)
-XX:MaxTenuringThreshold=15 # Old昇格までのGC回数を最大に
ログから以下を確認
grep "Pause Young" gc.log | wc -l # Young GC回数
grep "Pause Mixed" gc.log | wc -l # Mixed GC回数(減るべき)
grep "promotion failed" gc.log # 昇格失敗(ゼロが理想)
# Old世代の使用率推移
grep "Old:" gc.log
→ 徐々に増える場合は設定不足
🔧 Bean Validation を明示的に無効化
// Jersey設定内で
property("jersey.config.beanValidation.disable.server", true);
🔧 Validatorのプロデュース
@ApplicationScoped
public class ValidatorProducer {
private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private final Validator validator = factory.getValidator();
@Produces
@ApplicationScoped
public Validator produceValidator() {
return validator;
}
}
🔍 問題の本質:何が悪いのか
結論から言うと、「1つの機能が悪い」というより、Jersey + WebLogic + JAXB/MOXy + JSON-B/Jackson + CDI/HK2 の "複合要因"で、
JDK11 の GC と JIT の挙動が噛み合って クラス解決とアンロードが暴発している状態です。
⸻
✅ 何が悪いのか(本質)
システムの問題の核はこれです:
自動ディスカバリ(オートスキャン) と マルチスタック併用で、
大量のクラス解決・リフレクションが発生し、
Mixed GC の Class Unloading タイミングで一気にパージされる
そして
スレッド毎に JSON/Validation インフラが再生成されていた
| カテゴリ | 問題 |
|---|---|
| Jersey | プロバイダ自動検出、HK2、Validation自動起動 |
| JSON | MOXy + JSON-B + Jackson 混在 |
| JAXB | 自動 Context 探索動作 |
| CDI/HK2 | 混在でインジェクション経路が増殖 |
| GC/JIT | JDK11 の Class Unloading が重く動作 |
| JNI/JNA | Cモジュールもクラスローダーにぶら下がる |
→ この組み合わせが、
request毎に invisible な 反射・Registry 解決 → メモリ増加 → GCで爆発
という流れを作っていました。
⸻
✅ それはなぜ JDK8 では無事だったのか?
JDK11 から GC の仕様が変わっています:
| 項目 | JDK8 | JDK11 |
|---|---|---|
| Class Unloading | 遅いけど安定 | ZGC/G1で頻繁化・明示化 |
| JIT deopt | 少ない | プロファイル閾値が厳密化 |
| Reflection Cache | 比較的残る | GCで積極的にパージ |
JDK11 = class metadata flush がガチ
→ 自動検出 & リフレクション多用コードは不利
⸻
✅ なぜ GC 時だけスパイク?
「普段は速いが mixedGC で 2秒スパイク」
理由:
1. Request中に Provider/Context が積み上がる
2. Mixed GC で "クラスメタ" が大量に破棄
3. 次の request でそれを再生成
4. → resolve flood / oop flood → pause
⸻
✅ 発見された具体的悪玉
| 項目 | 悪さの内容 |
|---|---|
| JSON-B毎回create | スレッド毎に大きなオブジェクト生成 |
| MOXy context | 大量の JAXB metadata |
| Bean Validation | 気づかない自動起動で reflection |
| Jersey Provider AutoScan | ClassLoader探索 → cache肥大 |
| CDI + HK2 | 依存解決経路が増える |
| JNI(JNA) | クラスローダツリーに乗る |
どれも「単独なら問題ない」が
全部積むと JDK11 の GCと衝突する。
⸻
✅ だから改善策は何を狙っている?
| 改善内容 | 狙い |
|---|---|
| Provider手動登録 | 自動解決を殺して reflection止める |
| JSON/XML一元化 | Serializer stackを固定 |
| シングルトン化 | 毎回の巨大allocを排除 |
| Validation OFF or hand-rolled | 自動trigger排除 |
| JAXB/MOXy除外 | Class metadata削減 |
| CDI or HK2どちらかに固定 | Dependency graph縮小 |
| AutoScan無効 | Class scanning停止 |
→ GCで Class metadata を掃き出さない構成に変えている
⸻
✅ なぜまだ残る問題があるのか?
「少し改善したが決定打ではない」
→ オートディスカバリ系がまだ完全に殺せてない可能性が高い
特に、
ServerProperties.FEATURE_AUTO_DISCOVERY_DISABLEMETAINF_SERVICES_LOOKUP_DISABLE
この2つ未設定のままだと Jersey は水面下で勝手に探し続ける
✅ 最後に:一言で言うと?
負荷じゃなく、自動化された "クラス探索+リフレクション+キャッシュ破棄" が悪い
beanもpomから除外
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<exclusions>
<exclusion>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-bean-validation</artifactId>
</exclusion>
</exclusions>
</dependency>
✅ WebLogic利用は本来どうか?(JDK11 + javax + WebLogic)
本来避けるべき
| 理由 | 内容 |
|---|---|
| レガシー化 | WebLogicは Jakarta EE 以降の対応が遅い |
| クラウド非最適 | コンテナ/K8s時代と相性が悪い |
| 起動/デプロイ重い | Microservice向きでない |
| JAX-RS/MOXy問題 | 今回苦労したような依存の泥沼 |
✅ "本来"どう設計すべき?
| レイヤ | 推奨 |
|---|---|
| フロント | SPA(Vue/React/Next.js) |
| API | Helidon / Quarkus / Spring Boot |
| Java API | jakarta.* |
| 認証/Session | Token認証 / OAuth2 / OpenID Connect |
| デプロイ | Docker / K8s(EKS/AKS/GKE) |
| Messaging | Kafka / MQ |
| APサーバ | ❌ WebLogic → ✅軽量実装 |
最終系ではWebLogicは脱却がベスト