プログラマーとして、私たちは毎日コードを書いていますが、その ライフサイクル を本当に理解していますか?今日は、Java コードの誕生からゲームオーバーまでのライフヒストリーを簡単に説明します。大きく分けて、コンパイル、クラスのロード、実行、GC のステップに分かれます。
Java 言語のコンパイル期間は、実際には「不確実」なプロセスです。これは、.java ファイルを .class ファイルに変換するフロントエンド コンパイラーのプロセスである可能性もあるからです。 JVM へのバックエンド ランタイム コンパイラー (JIT コンパイラー) はバイトコードをマシンコードに変換します。また、静的先行コンパイラー (AOT コンパイラー) を使用して .java ファイルをローカルに直接コンパイルするプロセスを指す場合もあります。マシンコード。しかし、ここでは最初のカテゴリーについて話します。それはまた、編集に対する私たちの一般の理解とも一致しています。この間に編集はどのようなプロセスを経たのでしょうか?
字句解析はソースコードの文字列をトークンコレクションに変換することであり、構文解析はトークンシーケンスに基づいて抽象的に構文ツリー(ATS)を構築するプロセスです。プログラム コードの構文を記述するために使用されます。構文ツリーの各ノードは、パッケージ、型、修飾子、演算子、インターフェイス、戻り値などのプログラム コードの構文構造を表します。そしてコードコメントさえも文法構造です。
構文と字句解析が完了したら、次のステップはシンボルテーブルを埋めるプロセスです。シンボルテーブルに登録された情報は、コンパイルのさまざまな段階で使用されます。ここでシンボルテーブルの概念を拡張してみましょう。シンボルテーブルとは何ですか?これは、シンボル アドレスとシンボル情報のセットで構成される テーブル であり、最も単純な形式は、ハッシュ テーブルの K-V 値のペアとして理解できます。なぜシンボルテーブルが使用されるのですか?シンボル テーブルの最も初期の用途の 1 つは、プログラム コードに関する情報を整理することでした。当初、コンピューター プログラムは単なる単純な数字の文字列でしたが、プログラマーはすぐに、演算やメモリ アドレス (変数名) を表すために記号を使用する方がはるかに便利であることに気づきました。名前と番号を関連付けるには、シンボル テーブルが必要です。プログラムが大きくなるにつれて、シンボル テーブルの操作のパフォーマンスがプログラム開発効率のボトルネックになるため、シーケンス番号テーブルの効率を向上させるために多くのデータ構造とアルゴリズムが生まれました。いわゆるデータ構造やアルゴリズムとは何でしょうか?一般的に言えば: 順序付けされていないリンク リストでの逐次検索、順序付けされた配列での二分探索、二分探索ツリー、平衡探索ツリー (ここでは主に赤黒ツリーについて説明します)、ハッシュ テーブル (ジッパー方式に基づくハッシュ) リスト、ハッシュ テーブルリニアプロービングに基づいています)。 Java の java.util.TreeMap および java.util.HashMap と同様に、これらはそれぞれ赤黒ツリーのシンボル テーブルとジッパー ハッシュ テーブルに基づいて実装されます。ここではシンボルテーブルの概念については詳しく説明しませんので、興味のある方は関連情報を参照してください。
前の 2 つのステップの後、プログラム コードの抽象構文ツリー表現が得られました。構文ツリーは正しいソース コードの抽象化を表現できますが、ソース プログラムが論理的であることは保証できません。分析がステージに上がります。その主なタスクは、構造的に正しいソース プログラムの状況に応じたレビューを行うことです。アノテーションのチェック、データと制御フローの分析、および構文シュガーのデコードは、セマンティック分析段階のいくつかのステップです。ここでは、構文シュガーの概念について詳しく説明します。構文シュガーは、コンピューター言語に追加された特定の構文を指します。この構文は言語の機能には影響しませんが、プログラマーにとってはより便利です。 Java で最も一般的に使用される構文糖は、ジェネリック、可変長パラメーター、セルフボックス化/アンボックス化、およびトラバーサルループ です。JVM は、実行時にこれらの構文をサポートせず、コンパイル段階で単純な基本構文構造に戻ります。このプロセスは糖衣構文を解決することです。一般的な消去の例を挙げると、List
バイトコード生成は Javac コンパイルプロセスの最終段階であり、この段階では、前のステップで生成された情報がバイトコードに変換され、少量のコードの追加と変換も行われます。作業を行った。インスタンス コンストラクター
コンパイル プログラムをバイトコードにコンパイルしたら、次のステップはクラスをメモリにロードするプロセスです。
クラスロード処理は仮想マシンメモリのメソッド領域で行われ、仮想マシンメモリが関与するため、ここではまずメモリ領域でのプログラム配布の概念を簡単に紹介します。仮想マシンのメモリ領域は、プログラム カウンター、スタック、ローカル メソッド スタック、ヒープ、メソッド領域 (一部の領域はランタイム定数プール)、およびダイレクト メモリに分割されます。
プログラムカウンターは小さなメモリ空間であり、現在のスレッドによって実行されるバイトコードの行番号インジケーターとみなすことができます。 JVM コンセプト モデル では、バイトコード インタープリターは、このカウンターの値を変更して、実行する必要がある次のバイトコード命令を選択することによって機能します。
スタックは、ローカル変数テーブル、オペランドスタック、ダイナミックリンク、メソッド出口などの情報を格納するために使用されます。ローカル変数テーブルには、コンパイル中に制限されるさまざまな基本的なデータ型とオブジェクト参照が保存されます。プログラムカウンターと同様に、スレッドプライベートです。
ローカルメソッドスタックは上で紹介した仮想マシンスタックに似ていますが、それらの唯一の違いは、仮想マシンスタックは仮想マシンにJavaメソッド(バイトコード)の実行を提供するのに対し、ローカルメソッドスタックは仮想マシンスタックです。ネイティブ メソッド サービスによって使用され、一部の仮想マシンではこれら 2 つの部分が 1 つに結合されます。
ヒープは、JVMによって管理される最大のメモリ部分です。これはすべてのスレッドによって共有される領域であり、その唯一の目的は、オブジェクト インスタンスを格納することです (特殊クラス オブジェクトと同様に、メモリはメソッド領域に割り当てられます)。この場所は、メモリのリサイクルの観点から、ガベージ コレクション管理の主要な領域でもあります。現在、ガベージ コレクターは世代別コレクション アルゴリズム (後で詳しく説明します) を使用しているため、Java ヒープはさらに新世代と古い世代に分割できます。世代、および新しい世代 世代はさらに、Eden スペース、From Survivor スペース、To Survivor スペースに細分されます。効率上の理由から、ヒープは複数のスレッドプライベート割り当てバッファ (TLAB) に分割されることもあります。どのように分割されても、オブジェクト インスタンスはストレージの内容とは関係なく、メモリの再利用と割り当てを改善することだけが目的です。
メソッド領域は、ヒープと同様にスレッドが共有するメモリ領域で、クラス情報、定数、静的変数、ジャストインタイムコンパイラでコンパイルされたコードなどのデータを格納するために使用されます。仮想マシンによってロードされました。実行時定数プールはメソッド領域の一部であり、主にコンパイル時に宣言されたさまざまなリテラルとシンボル参照を格納するために使用されます。
ダイレクトメモリは、仮想マシンのランタイムデータ領域の一部ではなく、Java 仕様で定義されていないメモリ領域でもあり、単にメモリ割り当てが制限されていないと理解できます。 Java ヒープ サイズですが、Java ヒープ サイズ全体の制限によって制限されます。
仮想マシンのメモリ領域の概念について話した後、クラスロードプロセスとは何なのかという話題に戻りましょう。 5 つのステップ: ロード、検証、準備、解析、初期化。ロード、検証、準備、初期化は順番に実行されますが、解析は必ずしも初期化の後に実行されるとは限りません。
ロード段階では、JVM は 3 つのステップを完了する必要があります: まず、クラスの完全修飾名を通じてこのクラスを定義するバイナリ バイト ストリームを取得し、次にこのバイト ストリームによって表される静的ストレージ構造を変換します。メソッド領域への実行時データ構造、そして最後にこのクラスを表す java.lang.Class オブジェクトがメモリ内に生成され、メソッド領域でこのクラスのさまざまなデータ エントリとして機能します。バイナリ バイト ストリームを取得する最初のステップでは、*.class ファイルから取得することは明確に規定されていません。規制の柔軟性により、ZIP からバイナリ バイト ストリームを取得できます (JAR、EAR/WAR 形式の基礎を提供します)。 ) パッケージを作成し、ネットワークから取得 (アプレット)、実行時に計算して生成 (動的プロキシ)、その他のファイルを生成 (JSP ファイルによって生成されたクラス)、データベースから取得します。
検証は、名前が示すように、実際には、クラス ファイルのバイト ストリームに含まれる情報が JVM の要件を満たしていることを確認することです。これは、クラス ファイルのソースが必ずしもコンパイラから生成されるわけではなく、また、 16 進エディタを使用します。 クラス ファイルを直接書き込みます。検証プロセスには、ファイル形式の検証、メタデータの検証、およびバイトコードの検証が含まれます。ここでは、具体的なセキュリティ検証方法については詳しく説明しません。
準備 準備フェーズは、クラス変数に正式にメモリを割り当て、これらの変数が使用するメモリをメソッド領域に割り当てる段階です。 解析 解析フェーズは、JVM が定数プール内のシンボル参照を直接参照 (ターゲットへのポインタ、相対オフセット、またはハンドル) に置き換えるプロセスです。シンボルをコンパイルして埋める処理です。先ほど説明したテーブルがここに反映されています。解析プロセスは、クラス、インターフェイス、フィールド、およびインターフェイス メソッドを解析することに他なりません。 初期化 クラスの初期化フェーズは、準備フェーズで変数に初期値が割り当てられており、プログラマのカスタマイズされた要件に従ってクラス変数とその他のリソースが初期化されます。 。この段階では、前述のコンパイル済みバイトコード生成プロセスで説明したクラスを初期化して、その親クラスが初期化されていないことが判明した場合、その親クラスの初期化操作が最初にトリガーされます。
いよいよプログラムはデスステージに入ろうとしています。 JVM はプログラムの丸薬をどのように決定するのでしょうか?ここでは実際に到達可能性解析アルゴリズムを使用しています。このアルゴリズムの基本的な考え方は、「GC ルート」と呼ばれる一連のオブジェクトを開始点として使用し、このノードから下方向に探索するパスです。チェーンと呼ばれ、オブジェクトを GC ルートに接続する参照チェーンが存在しない場合 (グラフ理論の用語では、オブジェクトが GC ルートから到達できないことを意味します)、オブジェクトが利用できないことが証明され、リサイクル可能な物体。リサイクルするオブジェクトがすでにわかっている場合、いつガベージ コレクションをトリガーするのでしょうか?セーフティ ポイントは、GC を実行するためにプログラムが一時的に実行される場所です。このことから、GC の一時停止時間がガベージ コレクションの核心であることが簡単にわかります。すべてのガベージ コレクション アルゴリズムと派生ガベージ コレクターは、GC の一時停止時間を最小限に抑えることに重点を置いています。最新の G1 ガベージ コレクターは、予測可能な一時停止時間モデルを確立し、リージョン ガベージ コレクション全体での完全な操作を回避するように計画できます。以前にメモリ領域の分散の概念を紹介したときに、新しい世代と古い世代について説明しました。異なるガベージ コレクターが新しい世代または古い世代で動作する可能性があり、世代の概念さえありません (G1 コレクターなど)。 ) といいつつ、ここではガベージ コレクション アルゴリズムと対応するガベージ コレクターについて詳しく紹介します
最も基本的なコレクション アルゴリズムであり、アルゴリズムは 2 つの段階に分かれています: マーキングとクリア: 最初、マークされたすべての位置はリサイクルされる必要があります。 マークが完了すると、マークされたすべてのオブジェクトが均一にリサイクルされます。最大の欠点は、効率が悪く、大量の不連続メモリ フラグメントが生成されることです。これにより、プログラムがヒープ内に十分なメモリがある場合でも、十分な連続メモリを見つけることができない場合に問題が発生します。 GC 操作をトリガーします。ここで対応するガベージ コレクターは CMS コレクターです。
コピーアルゴリズムは、利用可能なメモリ容量を2つの等しいサイズのブロックに分割し、このメモリブロックが使い果たされたときに一度に1つだけを使用することができます。 オブジェクト を別のブロックにコピーし、使用されているメモリ領域を一度にクリーンアップします。こうすることで毎回半分の領域全体に対してGCが行われることになり、メモリの断片化などの問題は発生しません。現在の商用仮想マシンのほとんどは、このアルゴリズムを使用して新しい世代をリサイクルします。また、たとえば、Eden (1 つの Eden 領域) と Survivor (2 つの Survivor 領域) のデフォルトのサイズ比は 1 ではありません。 HotSpot は 8:1 です。Eden と Survivor 領域の 1 つが使用されるたびに、新しい世代で使用可能なメモリ領域は新しい世代全体の 90% になります。リサイクルする場合は、Eden といずれかの生存オブジェクトをコピーします。最後に、使用したばかりの Eden と Survivor スペースをクリーンアップします。コピー プロセス中に未使用の Survivor スペースが十分でない場合はどうなるかがわかります。現時点では、割り当て保証のために古い世代に依存する必要があります。保証が成功した場合、Eden と Survivor 内の生き残ったオブジェクトの 1 つが古い世代に移動されます。保証が失敗した場合は、ガベージ コレクションが行われます。古い世代でトリガーされます。この点を拡張すると、新世代のガベージ コレクションはマイナー GC と呼ばれます。これは、ほとんどの Java オブジェクトが生成されて消滅するため、マイナー GC が頻繁に発生し、回復速度が一般的に速いためです。旧世代のガベージ コレクションは、メジャー GC/フル GC と呼ばれます。メジャー GC の速度は一般にマイナー GC よりもはるかに遅いです。これまでの分析プロセスから、メジャー GC の発生にはマイナー GC が伴うことが多いと容易に推測できます。したがって、それが絶対的なものではありません。私たちの GC は実際には GC の速度を調整するためのものであり、Major GC の頻度を可能な限り制御して減らすのが最善です。ここで対応するガベージ コレクターは、シリアル コレクター、ParNew コレクター (後述する旧世代のコレクター CMS と連携できるシリアル コレクターのマルチスレッド バージョン)、および Parallel Scavenge コレクターです。
このアルゴリズムは、古い世代はコピー アルゴリズムほど頻繁にリサイクルされず、スペースも無駄にするため、古い世代のガベージ コレクションに使用されるアルゴリズムです。マーク整理プロセスは、後続のステップがリサイクル可能なオブジェクトを直接クリアするのではなく、生き残ったすべてのオブジェクトを一方の端に移動し、その後、端境界の外側のメモリを直接クリーンアップすることを除いて、マーククリアと似ています。ここで対応するガベージ コレクターは、Serial Old コレクターと Parallel Old コレクターです。
現在商用の仮想マシンはすべてこのアルゴリズムを使用しており、その考え方は、前述したようにヒープメモリ領域を世代に分割し、新しい世代と古い世代では異なる領域で異なるガベージコレクションアルゴリズムを使用することです。若い世代はコピー アルゴリズムを使用し、古い世代はマーク照合アルゴリズムまたはマーク スイープ アルゴリズムを使用します。
ここまでお話してきましたが、Java コードの歴史についてはある程度理解されているかもしれません。ここでは、サンプルを作成してプロセス全体を復習してみましょう。新しいオブジェクト、何を体験しますか?前述の内容と組み合わせると、JVM は新しい命令に遭遇すると、最初に命令パラメータ全体がメソッド領域の定数プール内のクラスのシンボル参照を見つけることができるかどうかをチェックし、シンボル全体で表されるクラスが存在するかどうかをチェックします。参照がロードされ、解析され、初期化されていない場合は、対応するクラスのロード プロセスを最初に実行する必要があります。クラスのロード チェックに合格すると、JVM は次に新しいオブジェクトにメモリを割り当てます。このプロセスは、クラスのロードが完了した後に行われます。ヒープ メモリが正常であれば、ポインタが使用されます。オブジェクトのサイズを移動するには、同じ距離で十分です。この割り当て方法は「ポインタ衝突」と呼ばれます。分散している場合、JVM は、使用可能なメモリを記録するリストを保持し、リストのレコードを割り当て、更新します。 free list」でどのメソッドが使用されるかは、前述したヒープにどのガベージ コレクターが使用されるかによって異なります。オブジェクト メモリを分割した後、仮想マシンは必要な初期化操作を実行します。次に、この情報をオブジェクト ヘッダーに設定する必要があります (クラス メタデータ情報、オブジェクト ハッシュ コード、オブジェクト GC 生成期間など)。 . )、これらのタスクが完了した後、新しいオブジェクトが生成されます。次のステップは、プログラマが計画したオブジェクト フィールドの割り当てを実行するための
以上がJava プログラムのライフヒストリーの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。