数日前、私は Alibaba のインターン生に面接を行い、Dalvik 仮想マシンがクラスファイルを実行できるかどうか尋ねられました。当時の私の答えは「いいえ」でしたが、クラスから変換された dex ファイルを実行することはできました。面接官がクラスファイルを実行できない理由を続けて尋ねたとき、私はDalvik仮想マシン内の最適化が原因であるとしか答えられず、具体的な理由を正しく答えることができませんでした。実際、周志明氏の本には答えがあります。Dakvik は Java 仮想マシンではありません。 Java 仮想マシン仕様に準拠していないため、Java クラス ファイルを実行できません。 JVM で一般的に使用されるスタック アーキテクチャではなく、レジスタ アーキテクチャを使用します。ただし、Java と密接な関係があり、実行する dex ファイルはクラス ファイルから変換できます。
実は、学部時代に「Java 仮想マシンの深い理解」という本に出会ったことはありましたが、じっくりと勉強したことはありませんでした。今振り返ってみると残念ですね!私は大学院1年生のときに、この勉強に多くの時間を費やしました。今、就職活動を準備しているのですが、読んでみたものの、内容をたくさん忘れていることに気づきました。この本の知識ポイントをまとめた記事を書くことにしました。もちろん、より詳しい内容をご覧になりたい場合は、「Java 仮想マシンの深い理解」をご覧ください。 JVM メモリ領域 プログラムを作成するときに、OOM (メモリ不足) やメモリ リークなどの問題に遭遇することがよくあります。これらの問題を回避するには、まず JVM のメモリ分割について具体的に理解する必要があります。 JVM はメモリをメソッド領域、仮想マシン スタック、ローカル メソッド スタック、ヒープ、プログラム カウンターに分割します。 JVM ランタイム データ領域は次のとおりです。 プログラムカウンタ プログラムカウンタはスレッド専用の領域なのでわかりやすいです。もちろん、各スレッドには現在実行中の命令を記録するためのカウンターが必要です。これは少量のメモリ空間を占有し、現在のスレッドによって実行されたバイトコードの行番号インジケータと見なすことができます。スレッドが Java メソッドを実行している場合、このカウンターは実行中の仮想マシン バイトコード命令のアドレスを記録します。ネイティブ メソッドが実行されている場合、このカウンターの値は空 (未定義) になります。このメモリ領域は、Java 仮想マシン仕様で OutOfMemoryError 条件が指定されていない唯一の領域です。 Java 仮想マシン スタック プログラム カウンターと同様に、Java 仮想マシン スタックもスレッド専用です。そのライフサイクルはスレッドのライフサイクルと同じです。仮想マシンスタックを理解するにはどうすればいいですか?本質的には、それはスタックです。そこに保存される要素はスタック フレームと呼ばれます。スタック フレームは非常に複雑に見えますが、実際には非常にシンプルです。関数のコンテキスト、具体的には実行された関数の一部のデータを格納します。実行された関数に必要なデータは、ローカル変数テーブル (関数内の変数を格納するため)、オペランド スタック (実行エンジンが計算するときに必要)、メソッドの終了などだけです。 実行エンジンが関数を呼び出すたびに、関数のスタック フレームが作成され、仮想マシン スタックに追加されます。別の観点から理解すると、各関数は、呼び出しから実行の終了まで、実際にはスタック フレームのプッシュとポップに対応します。 この領域では 2 つの例外が発生する可能性があることに注意してください。1 つは StackOverflowError で、これは現在のスレッドによって要求されたスタック深度が仮想マシンで許可されている深度よりも大きい場合にスローされます。この例外の作成は簡単です。関数を繰り返し再帰的に呼び出すと、最終的にスタック オーバーフロー エラー (StackOverflowError) が発生します。もう 1 つの例外は OutOfMemoryError 例外です。仮想マシン スタックを動的に拡張できる場合 (現在のほとんどの仮想マシンは可能)、十分なメモリを適用できない場合は、OutOfMemoryError がスローされます。仮想マシンスタックを OOM にする方法は?コードを参照してください:
このコードは危険であり、オペレーティング システムがフリーズする可能性があります。ご注意の上ご使用ください〜〜〜 ネイティブメソッドスタック ローカル メソッド スタックと仮想マシン スタックが果たす役割は非常に似ています。それらの違いは、仮想マシン スタックは Java コード メソッドを実行するために使用され、ローカル メソッド スタックはネイティブ メソッドに使用されることです。仮想マシン スタックと同様に、ローカル メソッド スタックも StackOverflowError および OutOfMemoryError 例外をスローする可能性があります。 Javaヒープ Java ヒープは、仮想マシン内の最後のメモリ部分であると言えます。これはすべてのスレッドで共有されるメモリ領域であり、ほぼすべてのインスタンス オブジェクトがこの領域に格納されます。もちろん、JIT コンパイラの開発により、すべてのオブジェクトをヒープ上に割り当てることは「絶対的」ではなくなってきています。 Java ヒープは、ガベージ コレクターによって管理される主要な領域です。現在のコレクターは基本的に世代別コレクション アルゴリズムを使用するため、すべての Java ヒープは新しい世代と古い世代に分けられます。詳細には、新世代は、エデン空間、From Survivor 空間、To Survivor 空間に分かれています。ヒープを拡張できなくなると、OutOfMemoryError 例外がスローされます。 方法領域 メソッド領域には、クラス情報、定数、静的変数などが格納されます。メソッド領域は、スレッドごとに共有される領域です。 Java コードを記述すると、各スレッドが同じクラスの静的変数オブジェクトにアクセスできることが簡単にわかります。リフレクション機構を使用しているため、仮想マシンはクラス情報が使用されなくなったことを推測することが難しく、この領域を再利用することが困難です。また、このエリアは主に一定のプール回復を目的としています。 JDK1.7 では定数プールがヒープへ転送されたことは注目に値します。同様に、メソッド領域がメモリ割り当て要件を満たせない場合は、OutOfMemoryError がスローされます。 メソッド領域のメモリオーバーフローが発生します。これは JDK 1.6 以前のバージョンでのみ発生することに注意してください。理由は後ほど説明します。実行前に、仮想マシン パラメータ -XXpermSize および -XX:MaxPermSize を設定して、メソッド領域のサイズを制限できます。
実行後、java.lang.OutOfMemoryError:PermGen スペース例外がスローされます。 説明すると、String の intern() の機能は、現在の文字列が定数プールに存在しない場合に、それを定数プールに入れることです。上記のコードは定数プールに文字列を継続的に追加するため、最終的にはメモリ不足に陥り、メソッド領域で OOM が発生します。 上記のコードを JDK1.6 より前に実行する必要がある理由を次に説明します。前述したように、JDK 1.7 以降では定数プールがヒープ領域に配置され、intern() 関数の機能が変更されます。次のコードを見て、どのように動作するかを見てみましょう。
このコードは、JDK 1.6 と JDK 1.7 では異なる方法で実行されます。 JDK1.6 の結果は false、false であり、JDK1.7 の結果は true、false です。その理由は、JDK1.6 では、intern() メソッドが *** が遭遇した文字列インスタンスを定数プールにコピーし、定数プール内の文字列への参照を返すためです。 StringBuilder によって作成された文字列インスタンスはヒープ上にあるため、同じ参照ではないはずであり、false を返します。 JDK1.7 では、 intern はインスタンスをコピーしなくなり、最初のインスタンスへの参照のみが定数プールに保存されます。したがって、intern() によって返される参照は、StringBuilder によって作成された文字列インスタンスと同じです。 str2 の比較で false が返されるのはなぜですか?これは、クラスが JVM 内で内部的にロードされるときに、文字列「java」がすでに存在し、「*** が出現する」原則を満たしていないため、false が返されるためです。 ガベージコレクション (GC) JVM のガベージ コレクション メカニズムでは、オブジェクトがデッドであるかどうかは、それを参照しているオブジェクトがまだ存在するかどうかではなく、到達可能性分析によって決定されます。オブジェクト間の参照はツリー構造に抽象化できます。ツリーのルート (GC ルート) から開始して、下方向に検索が行われます。走査されるチェーンは参照チェーンと呼ばれます。オブジェクトを GC ルートに接続する参照チェーンが存在しない場合は、そのオブジェクトは使用できないことが証明され、リサイクル可能なオブジェクトとして判断されます。 では、GC ルートとして使用できるオブジェクトは何でしょうか?主に以下のものがあります。 1. 仮想マシン スタック内で参照されるオブジェクト (スタック フレーム内のローカル変数テーブル)。 2. メソッド領域内のクラスの静的属性によって参照されるオブジェクト。 3. メソッド領域内の定数によって参照されるオブジェクト 4. ローカル メソッド スタック内の JNI によって参照されるオブジェクト (一般にネイティブ メソッドと呼ばれる)。 さらに、Java ではソフト参照と弱参照も提供されます。これら 2 つの参照は、仮想マシンによっていつでも再利用できるオブジェクトです。ビットマップ オブジェクトなど、メモリを多く消費するが後で使用される可能性のあるオブジェクトを、ソフト参照または弱参照として宣言できます。ただし、エラーを回避するために、このオブジェクトを使用するたびに、それが null かどうかを明示的に確認する必要があることに注意してください。 3つの一般的なガベージコレクションアルゴリズム 1. マークアンドスイープアルゴリズム まず、リサイクル可能なオブジェクトが到達可能性分析によってマークされ、その後、マークされたすべてのオブジェクトがマーク後に均一にリサイクルされます。マーキング プロセスは、実際には到達可能性分析のプロセスです。この方法には 2 つの欠点があります。1 つは効率の問題です。マーキングとクリアリングの両プロセスの効率は高くありません。もう一つはスペースの問題です。マーキングとクリアを行うと、不連続なメモリフラグメントが大量に生成されます。 2. コピーアルゴリズム 効率の問題を解決するために、コピー アルゴリズムはメモリを同じサイズの 2 つのブロックに分割し、一度にそのうちの 1 つだけを使用します。このメモリ ブロックが使い果たされると、残っているオブジェクトは別のメモリ ブロックにコピーされます。次に、使用済みのメモリをすぐにクリーンアップします。つまり、毎回ガベージ コレクションされるのは領域の半分だけであり、メモリを割り当てるときにメモリの断片化を考慮する必要はありません。 ただし、このコストは実際には受け入れられず、一般的なメモリ領域を犠牲にする必要があります。研究により、ほとんどのオブジェクトは「一夜にして生まれて死ぬ」ため、メモリ空間を 1:1 の比率で分割する必要がないことが判明しました。代わりに、メモリは、より大きな Eden スペースと 2 つのより小さな Survivor スペースに分割されます。毎回、エデンスペースとサバイバースペース 1 つが使用されます。デフォルトの比率は、Eden: Survivor = 8:1 です。新世代エリアはこのように分けられています。毎回、Eden にインスタンスが 1 つと Survivor スペースが 1 つ割り当てられます。リサイクルされると、生き残ったオブジェクトは残りの Survivor スペースにコピーされます。この方法では、メモリの 10% のみが無駄になりますが、効率は非常に高くなります。残りの Survivor メモリが不足する場合、古い世代のメモリを使用して割り当て保証を行うことができます。割り当て保証をどのように理解すればよいですか?実際には、メモリが不足している場合は、古い世代のメモリ空間に割り当てに行き、新しい世代のメモリが回復した後、メモリを古い世代に戻し、新しい世代で Eden:Survivor=8:1 を維持します。さらに、2 人の生存者にはそれぞれ「From Survivor」と「To Survivor」という名前があります。 2 つの ID は頻繁に入れ替わります。つまり、このメモリ ブロックが Eden と一緒に割り当てられる場合もあれば、別のブロックに割り当てられる場合もあります。なぜなら、彼らはお互いを模倣することが多いからです。 3. マークソートアルゴリズム マークスイープアルゴリズムは非常にシンプルです。まずリサイクルする必要があるオブジェクトをマークし、次に残っているすべてのオブジェクトをメモリの一方の端に移動します。これの利点は、メモリの断片化を回避できることです。 クラスローディングメカニズム クラスのライフ サイクル全体は、仮想マシンのメモリにロードされることから始まり、メモリからアンロードされることで終了します。ライフサイクル全体には、読み込み、検証、準備、解析、初期化、使用、アンロードの 7 つの段階が含まれます。 ロード、検証、準備、初期化、アンロードの 5 つの段階の順序は固定されています。解析フェーズは必ずしもそうである必要はありません。Java ランタイム バインディングをサポートするために、場合によっては初期化フェーズの後に開始されることもあります。 初期化について: JVM 仕様では、クラスの初期化を実行する必要がある状況は 5 つだけであると明確に規定されています (当然、その前に読み込み、検証、準備を行う必要があります)。 1. new、getstatic、putstatic、invokestatic に遭遇したとき、クラスが初期化されていない場合は、初期化する必要があります。これらの命令は、新しいオブジェクト、静的変数の読み取り、静的変数の設定、および静的関数の呼び出しを参照します。 2. java.lang.reflectパッケージのメソッドを使用してクラスを反映する場合、クラスが初期化されていない場合は初期化する必要があります。 3. クラスを初期化するときに、親クラスが初期化されていないことが判明した場合は、まず親クラスの初期化をトリガーする必要があります。 4. 仮想マシンが起動すると、ユーザーは実行するメイン クラス (メイン関数を含むクラス) を指定する必要があり、仮想マシンは最初にこのクラスを初期化します。 5. ただし、JDK1.7 で有効になった動的言語サポートを使用する場合、MethodHandle インスタンスの最初の解析の結果が REF_getStatic、REF_putStatic、または Ref_invokeStatic のメソッド ハンドルであり、このメソッド ハンドルに対応するクラスが初期化されていない場合は、まずその初期化をトリガーする必要があります。 また、サブクラスを介して親クラスの静的フィールドを参照しても、サブクラスは初期化されないことに注意してください。
*** は以下のみを出力します: SuperClass init! 静的変数の場合、このフィールドを直接定義するクラスのみが初期化されます。したがって、親クラスで定義された静的変数をサブクラスを通じて参照すると、親クラスの初期化のみがトリガーされ、サブクラスの初期化はトリガーされません。 配列定義を通じてクラスを参照しても、クラスの初期化はトリガーされません。
定数はコンパイル中に呼び出し元の定数プールに格納されます。本質的には、定数を定義するクラスを直接参照しないため、定数を定義するクラスの初期化はトリガーされません。サンプルコードは次のとおりです。
上記のコードには ConstClass init は表示されません。 負荷 読み込みプロセスは主に次の3つのことを行います。 1. 完全修飾名でクラスのバイナリストリームを取得する 2. このバイトストリームによって表される静的ストレージ構造をメソッド領域のランタイムデータ構造に変換する 3. メソッド領域でこのクラスのさまざまなデータにアクセスするためのエントリ ポイントとして、このクラスを表す java.lang.Class オブジェクトをメモリ内に生成します。 確認する この段階では主に、クラス ファイルのバイト ストリームに含まれる情報が現在の仮想マシンの要件を満たし、仮想マシン自体のセキュリティを危険にさらさないことを確認します。 準備する 準備フェーズは、クラス変数にメモリを正式に割り当て、クラス変数の初期値を設定するフェーズです。これらの変数によって使用されるメモリはメソッド領域に割り当てられます。まず、このとき割り当てられるメモリにはインスタンス変数ではなく、クラス変数(static によって変更される変数)のみが含まれます。インスタンス変数は、オブジェクトがインスタンス化されるときに、オブジェクトとともに Java ヒープ内に割り当てられます。次に、ここで言及されている初期値は「通常」データ型のゼロ値です。クラス変数が次のように定義されているとします。
準備段階後の変数値の初期値は、Java メソッドがまだ実行されていないため、123 ではなく 0 になります。プログラムがコンパイルされ、クラス コンストラクター () メソッドに格納された後、値は 123 に割り当てられます。 分析 解析フェーズは、仮想マシンの定数プール内のシンボリック参照を直接参照に置き換えるプロセスです。 初期化 クラスの初期化は、クラスのロードの最後のステップです。以前のクラス ローディング プロセスでは、ユーザーがカスタム クラス ローダーを通じて参加できるローディング ステージを除き、残りのアクションは仮想マシンによって支配され、制御されます。クラスで定義された Java プログラム コードが実際に実行されるのは、初期化フェーズです。 準備フェーズでは、変数にシステムに必要な初期値が一度割り当てられていますが、初期化フェーズでは、クラス変数はプログラマーがプログラムを通じて作成した主観的な計画に従って初期化されます。初期化プロセスは、実際にはクラスのconstructor() メソッドを実行するプロセスです。 () メソッドは、クラス内のすべてのクラス変数割り当てアクションを自動的に収集し、静的ステートメント ブロック内のステートメントをマージするコンパイラによって生成されます。コレクションの順序は、ソース ファイル内でステートメントが出現する順序です。静的ステートメント ブロックでは、静的ステートメント ブロックの前に定義された変数にのみアクセスできます。それ以降に定義された変数には値を割り当てることはできますが、アクセスすることはできません。以下のように表示されます。
() メソッドは、クラス コンストラクター (またはインスタンス コンストラクター ()) とは異なります。親クラスのコンストラクターを明示的に呼び出す必要はありません。仮想マシンは、子クラスの () メソッドが実行される前に、親クラスの () メソッドが実行されていることを確認します。 クラスローダー ここではカスタム クラス ローダーと親委任モデルについては説明しません。何時間も書き続けたので、そろそろ寝る時間です。 |
<<: クラウドコンピューティング支出の35%が無駄になっている理由
Racknerd は、米国の VPS 料金を年間 10.88 ドルからという低価格で提供する大規模な...
顧客の観点から見ると、クラウド サービスは問題なく機能するはずです。ただし、サービスの中断は避けられ...
新しく設立された VPS マーチャントである Fastmako は、主にオランダのデータセンターで ...
5G ネットワークとリモート操作への大幅な移行により、エッジ コンピューティングは企業のデジタル変革...
Yer hosting は 2005 年に設立されました。アゼルバイジャンの会社で、主にアゼルバイジ...
net2hosting は新しいホスティング プロバイダーです。10 ドルを支払っても構わないなら、...
エコノミック・ボイスによると、グループ購入業界は2012年に200億元規模に達するだろう。これは、M...
月収10万元の起業の夢を実現するミニプログラム起業支援プランSEO 編集者は、特に Baidu Be...
[[436836]] Tekton は、非常に強力で柔軟な CI/CD オープンソース クラウド ネ...
数日前にも述べたように、SEO は技術ではなく、技術だけではない、という思いが強くなってきています。...
2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますこれらのデ...
みなさんこんにちは。私はNezhaです。今日は、Kubernetes マルチクラウドの実装についてお...
ワトソンズは、正確な消費者グループの位置付け、専門的なサービス、高品質で低価格の自社ブランド製品、専...
2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています著者: D...
何らかの理由で、元のサーバースポンサーがネットワークケーブルを抜いてしまい、ウェブサイトにアクセスで...