遅いデバッガーのトラブルシューティング
他の言語: English Español 한국어 Português 中文
一般的にはオーバーヘッドが最小限ですが、 Javaデバッガーは特定の状況下で大幅なランタイムコストを発生させることがあります。 最悪の場合、デバッガーはVMを完全にフリーズさせることさえあります。

これらの問題の原因と可能な解決策を調査してみましょう。
この記事で述べられている特性はJetBrainsのIDEに関しています。 そのため、他のツールでは一部の機能が利用できないかもしれません。 しかし、一般的なトラブルシューティングの戦略は引き続き適用されます。
原因の診断
可能な解決策を探る前に、問題を特定することが賢明です。 デバッガーがアプリケーションを遅くする最も一般的な理由は以下の通りです:
- メソッドのブレークポイントの使用
- 式を頻繁に評価する
- 計算用途が重たすぎる式を評価する
- レイテンシーが高いリモートデバッグ
IntelliJ IDEAは、デバッガーのパフォーマンス問題の原因を明確に特定するために、 デバッガーのオーバーヘッド (Overhead)タブで詳細な統計を提供します:


これにアクセスするには、レイアウト設定 (Layout Settings)タブからオーバーヘッド (Overhead)を選択します。 オーバーヘッド (Overhead)タブは、ブレークポイントやデバッガーの機能のリストを表示します。 各ブレークポイントや機能の横には、各デバッガー機能が使用された回数と、実行にかかった時間が表示されます。
オーバーヘッド (Overhead)タブでは、リソースを消費する機能を一時的にオフにすることもできます。 その機能の対応するチェックボックスのチェックを外すことでできます。
パフォーマンス問題の発生源を特定する方法を見てきましたので、 最も一般的な原因とその対策について見ていきましょう。
メソッドのブレークポイント
Javaでメソッドのブレークポイントを使用すると、使用しているデバッガーによってはパフォーマンスが低下することがあります。 これは、Java Debug Interfaceが提供する対応する機能が特に遅いためです。
この問題を解決するために、IntelliJ IDEAはエミュレートされたメソッドのブレークポイントを提供します。 これらは通常のメソッドのブレークポイントと同様に機能しますが、より効率的に動作します。 エミュレートされたメソッドのブレークポイントは、実際のメソッドのブレークポイントを設定する代わりに、 IDEがプロジェクト全体のメソッドの実装内に通常のラインのブレークポイントを置き換えるというトリックを用いています。
デフォルトでは、IntelliJ IDEAの全てのメソッドのブレークポイントはエミュレートされています:


この機能を持たないデバッガーを使用していて、メソッドのブレークポイントでパフォーマンスの問題が発生した場合、 同じトリックを手動で行うことができます。 全てのメソッドの実装を訪れるのは面倒かもしれませんが、デバッグ中の時間を節約することで合理化される可能性があります。
「エミュレートされたメソッドのブレークポイントのためのクラスの処理」が長すぎる
メソッドが非常に多くの実装を持っている場合、 そのメソッドにメソッドのブレークポイントを設定することは時間がかかるかもしれません。 この場合、IntelliJ IDEAとAndroid Studioは、 エミュレートされたメソッドブレークポイントの処理クラス (Processing classes for emulated method breakpoints)と表示するダイアログが表示されます。
もし、メソッドのブレークポイントのエミュレーションの処理があまりにも長い場合は、ラインのブレークポイントの使用を検討してみてください。 また、ブレークポイントの設定でエミュレート対象 (Emulated)チェックボックスのチェックを外すことで、 いくらかのランタイムパフォーマンスを犠牲にすることもできます。
ホットコード内の条件付きブレークポイント
ホットコード内に条件付きブレークポイントを設定すると、デバッグセッションが大幅に遅くなる可能性があります。 これは、このコードがどれだけ頻繁に実行されるかによります。
次のコードスニペットを考えてみましょう:
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
}
const val ITERATIONS = 100_000
fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") }
sum += i
にブレークポイントを設定し、
条件として false
を指定しましょう。
これは、デバッガーがこのブレークポイントで停止しないようにする、ということを具体的に意味します。
しかし、このラインが実行されるたびに、デバッガーは false
を評価しなければなりません。


この場合、このコードをブレークポイントがある状態とない状態で実行した結果はそれぞれ39 ms
と29855 ms
でした。
驚くべきことに、たった100,000回のイテレーションしかないにも関わらず、その差は大きいです!
一見些細な条件である false
の評価にこれほどの時間がかかることは驚きかもしれません。
これは、経過時間が表現の結果の計算だけでないためです。
デバッガーのイベントの処理とデバッガーのフロントエンドとの通信も含まれています。
解決策は直截です。 条件を直接アプリケーションコードに統合することができます:
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
if (false) { // condition goes here
System.out.println("break") // breakpoint goes here
}
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
}
fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
if (false) { // condition goes here
println("break") // breakpoint goes here
}
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") }
この設定では、VMが条件のコードを直接実行し、このコードを最適化することさえあります。 それに対して、デバッガーはブレークポイントに達した時点でのみ動作します。 ほとんどのケースでは必要ないかもしれませんが、この変更により、もし、 ホットパスの途中で条件によりプログラムを一時停止する必要がある場合、時間を節約できます。
この技術は、ソースコードが利用可能なクラスには完璧に機能します。 しかし、コンパイルされたコード、例えばライブラリについては、このトリックがうまくいかない可能性があります。 これは特別なユースケースで、別の議論でカバーする予定です。
暗黙の評価
自分で式を指定する機能(例:ブレークポイントの条件、ウォッチなど)に加えて、 あなたのために暗黙的に式を評価する機能もあります。
以下に例を示します:


プログラムを一時停止するたびに、デバッガーは現在のコンテキストで利用可能な変数の値を表示します。 一部の型は、表示やナビゲートが困難な複雑な構造を持つことがあります。 あなたの便宜のために、デバッガーはそれらをレンダラーと呼ばれる特別な式を使用して変換します。
レンダラーは toString()
のように単純なものもあれば、
コレクションの内容を変換するような複雑なものもあります。それらは組み込みまたはカスタムであることがあります。
IntelliJ IDEAのデバッガーはデータの表示方法に関して非常に柔軟です。 IDEは、アノテーションを使用してレンダラーを設定することさえ許可しており、 複数の寄稿者がいるプロジェクト全体で一貫したクラスの表現を保証します。
データの表示形式を設定する方法について詳しくは、 IntelliJ IDEAのドキュメンテーションを参照してください。
通常、デバッグレンダラーによるオーバーヘッドは無視できる程度ですが、
影響は最終的に特定のユースケースに依存します。
事実、あなたの toString()
の実装の一部が暗号通貨のマイニング用のコードを含んでいた場合、
デバッガーはそのクラスの toString()
の値を表示するのに苦労するでしょう!
特定のクラスのレンダリングが遅い場合、あなたは 対応するレンダラーをオフにすることができます。 より柔軟な代替策として、 あなたは必要に応じてレンダラーを設定することができます。オンデマンドのレンダラーは、 明示的にその結果を表示するようリクエストしたときにのみ実行されます。
リモートデバッグセッションでの高レイテンシ
技術的な観点から見れば、リモートアプリケーションのデバッグはローカルでのデバッグと何ら変わりません。 どちらの場合も、接続はソケット経由で確立されます — ここでは 共有メモリモード はこの議論から除外します。そして、デバッガーはホストJVMがどこで実行されているかさえ知りません。
しかし、リモートデバッギングに固有の要素が一つあるかもしれません、それはネットワークのレイテンシです。 特定のデバッガーの機能はそれぞれの使用時に複数のネットワークのラウンドトリップを行います。 これが高レイテンシと組み合わさると、デバッギングのパフォーマンスが大幅に低下する可能性があります。
その場合は、プロジェクトをローカルで実行することを考えてみてください、それにより多くの時間を節約できるでしょう。 それ以外の場合、一部の高度な機能を一時的にオフにすることで利益を得るかもしれません。
まとめ
この記事では、デバッガーのパフォーマンスが低下する最も一般的な問題の修正方法を学びました。 IDEが時々それをあなたの代わりに処理してくれることもありますが、基礎となるメカニズムを理解することが重要です。 これにより、日々のデバッグ作業でより柔軟で効率的で創造的になることができます。
これらのヒントとトリックが役立ったと思います。いつも通り、あなたのフィードバックを大いに歓迎します! Twitterや LinkedIn、 Telegramで気軽にご連絡ください。
ハッピーデバッギング!