ヒープとスタックについては誰もが知っていますが、なぜこれらが存在するのかは知りません。この記事ではその原理について説明します
目次
簡単に言うと、Java はヒープとスタックを分割してメモリを有効に利用し、メモリ使用効率を向上させます。
1. ヒープとは何か、スタックとは何か
Java のヒープ (Heap) とスタック (Stack) は、メモリ内の 2 つの異なる記憶領域であり、プログラムの実行中にデータを保存するために使用されます。
1.1 ヒープ
ヒープはツリーベースのデータ構造です。オブジェクトと配列のサイズは固定されていないため、スタック メモリに格納できません。そうしないとスタック メモリが不足するため、ヒープはオブジェクトとその配列を格納する場所です。インスタンス変数。ヒープのサイズは固定ではなく、仮想マシンによって自動的に管理されるため、ヒープ領域が不足すると OutOfMemoryError 例外がスローされます。ヒープメモリは動的な割り当てと解放を特徴とするため、オブジェクトや配列のサイズを動的に調整でき、ヒープメモリの利用効率が比較的高いです。同時に、ヒープメモリが不連続であるため、アクセス速度は比較的遅くなります。
ヒープがツリー データ構造であるのは、主に効率的な動的メモリ割り当てと割り当て解除をサポートする必要があるためです。ヒープでは、メモリ空間はツリー構造で編成され、各ノードはメモリ ブロックのサイズとステータス (割り当て済みまたは未割り当て) を含むメモリ ブロックを表します。
ヒープのツリー構造により、効率的なメモリ割り当てと回復操作をサポートできます。メモリの一部を割り当てる必要がある場合、ヒープはルート ノードから開始してツリーを走査し、十分な大きさの未割り当てのメモリ ブロックを探し、それをプログラムに割り当てます。同様に、メモリ ブロックを解放する必要がある場合、ヒープはメモリ ブロックの位置から開始され、隣接する未割り当てメモリ ブロックを再帰的にマージしてより大きな未割り当てメモリ ブロックを形成し、最後にメモリ ブロックを未割り当てステータスに復元します。後続のプログラムはそれを引き続き使用できます。
ヒープは動的なデータ構造であるため、メモリの割り当てと回復は頻繁に行われます。ツリー構造はスケーラビリティと柔軟性に優れているため、ヒープは効率的なメモリ割り当てと回復をサポートするツリー構造として設計されています。
1.2スタック
スタックは、基本的な型とオブジェクトへの参照 (つまり、ヒープ内のオブジェクトの格納場所へのポインター) を格納する先入れ後出しのデータ構造です。各スレッドには独自のスタック領域とスタックのサイズがあります。固定されており小さいです。仮想マシンによって自動的に管理されます。スタック領域が不十分な場合、StackOverflowError 例外がスローされます。メソッドが呼び出されると、スタック メモリ内にスタック フレームが作成され、メソッド パラメータ、ローカル変数、メソッドの戻り値などの情報が格納されます。メソッドの実行が完了すると、スタック フレームがポップされ、スタック メモリから削除されます。スタックメモリのデータ格納方式は連続的であるため、アクセス速度が比較的速く、同時にスタックフレームの動的な生成と破壊によりスタックメモリの管理が比較的容易です。
スタックは、スタックの最上部でのみ挿入および削除できる線形データ構造です。スタックでは要素の挿入と削除はスタックの先頭でのみ行われるため、この操作は配列構造を使用することで簡単に実現できます。配列は任意の位置の要素に直接アクセスできるため、スタックの先頭で要素を挿入および削除する場合は、配列の最後で挿入および削除するだけで済みます。これにより、操作の時間計算量が O(1 になります) )。同時に、配列構造を使用すると、頻繁なメモリの割り当てと解放によって生じる時間とスペースのオーバーヘッドを回避し、スタックの効率を向上させることもできます。したがって、スタックは通常、配列構造を使用して実装されます。
1.3 概要
すべてのスレッドは同じヒープ領域を共有し、その中のオブジェクトはすべてのスレッドからアクセスできますが、スタック領域は独立しており、各スレッドは独自のスタック領域を持っています。
- Java プログラムが最初に javac によってバイトコードにコンパイルされると、クラス ローダーを通じてメモリにロードされ、実行エンジンがこれらのバイトコード命令を読み取って実行します。
- プログラムが開始されると、JVM はメイン スレッドを作成し、仮想マシン スタック、ローカル メソッド スタック、スレッドのプログラム カウンターなどのプライベート領域を作成します。
- メソッドが呼び出されると、実行エンジンはメソッドに関する情報を含む新しいスタック フレームを作成し、それを仮想マシン スタックの最上位にプッシュします。オブジェクトを作成する必要がある場合、メモリがヒープに割り当てられ、そのオブジェクトへの参照がスタックに配置されます。静的変数と静的定数はメソッド領域に格納されます。
- 実行が完了すると、仮想マシン スタックの最上位にあるスタック フレームがポップされます。プログラムの実行中にガベージ コレクションが自動的に実行され、未使用のメモリが再利用されます。
2. ヒープに何が保存されるか
2.1 Javaクラスインスタンスオブジェクト
「new」キーワードを使用してオブジェクトが作成されると、オブジェクトはヒープに保存されます。
例如:`MyClass obj = new MyClass();` 中的 `obj` 对象就存储在堆中。
2.2 配列オブジェクト
Java の配列もオブジェクトであり、配列オブジェクトもヒープに格納されます。
例如:`int[] arr = new int[5];` 中的 `arr` 数组对象就存储在堆中。
2.3 オブジェクトインスタンスのメンバ変数
Java オブジェクトに他のオブジェクト型のメンバー変数が含まれている場合、これらのメンバー変数もヒープに格納されます。
例如: `MyClass` 类中,`value` 对象就存储在堆中。
public class MyClass {
private String value;
// ...
}
2.4 静的変数
Javaの静的変数はメソッド領域に格納されますが、その初期値はクラスロード時にヒープに格納されます。
例如: `MyClass` 类中的 `count` 静态变量的初始值就存储在堆中。
public class MyClass {
private static int count = 0;
// ...
}
2.5 定数プール:
コンスタントプールは非常に特殊です。ここにいくつかの興味深い点があります
2.5.1 興味深い定数プール
文字列がString
classを使用して作成された場合literal
、文字列オブジェクトは定数プールに格納されます。たとえば、次の文字列は両方ともliteral
を使用して作成されたため、定数プールに保存されます。
String str1 = "hello";
String str2 = "hello";
str1 = "hello" と str2 = "hello" は両方ともリテラル値として作成された文字列オブジェクトであるため、文字列定数プールに格納されます。これらは同じ値を持つため、実際には同じ文字列定数オブジェクトを参照します。したがって、str1 と str2 は実際には同じオブジェクトを参照します。
クラスのnew
演算子やコンストラクター を使用するなど、他の手段で作成された文字列オブジェクトの場合、文字列オブジェクトはヒープに格納されます。String
この場合、文字列オブジェクトへの参照はstr3
スタックに保存され、文字列の実際の内容はヒープに保存されます。str3 と str1 はオブジェクトではありません。例えば:
String str3 = new String("hello");
このコードでは、文字列 "hello" が最初に定数プールに存在するかどうかを確認します。存在しない場合は、新しい文字列オブジェクトが定数プールに作成され、次に新しい文字列オブジェクトが作成されます。定数プール の文字列オブジェクトへの参照が、この新しい文字列オブジェクトに渡されます。new キーワードを使用しているため、この新しい文字列オブジェクトはヒープ メモリに領域を割り当てます。したがって、このコードでは、「hello」文字列によって定数プールにオブジェクトが作成され、ヒープ メモリに新しいオブジェクトが作成されます。
3. スタックには何が保存されますか
Java では、各スレッドに独自のスタックがあります。スタックは後入れ先出し (LIFO) データ構造であり、通常は配列またはリンク リストを使用して実装されます。メソッドが呼び出されるとき、パラメータ、ローカル変数、メソッドの戻りアドレスなどの情報がスタックの先頭にプッシュされ、メソッドが戻るときに情報がスタックからポップされます。
それでは、Java コードを例として、スタックに格納される可能性のある内容を簡単に紹介しましょう。
public class StackExample {
public static void main(String[] args) {
int a = 1;
String b = "hello";
int[] arr = {1, 2, 3};
System.out.println(a + b + arr[0]);
}
}
1. `args` パラメータ配列への参照。これは、プログラムの実行時に main メソッドに渡されるパラメータを含む文字列配列を指します。
2. 基本データ型の整数である「a」変数はスタックに格納されます。
3. 文字列オブジェクトへの参照である「b」変数はスタックに格納され、ヒープ内の文字列オブジェクトを指します。
4. 整数型の配列オブジェクトへの参照である `arr` 変数はスタックに格納され、ヒープ内の整数配列オブジェクトを指します。
5. 「main」メソッドのコールスタックフレームには、メソッドパラメータ、ローカル変数、オペランドスタックなどの情報が含まれます。
メソッドが呼び出されるたびに新しいスタック フレームが作成されるため、スタックにはいくつかのインタビュー ポイントもあります
3.1 興味深いスタック
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.setValue(5);
System.out.println(obj.getValue()); // 输出5
foo(obj);
System.out.println(obj.getValue()); // 输出10
}
public static void foo(MyClass obj) {
//obj = new MyClass();
obj.setValue(10);
}
MyClass
このコードでは、整数プロパティを含むクラス を定義しvalue
、そのプロパティのメソッドにアクセスします。このメソッドでは、オブジェクトmain
を作成し、そのプロパティを 5 に設定します。次に、 というメソッドを呼び出し、これをパラメータとして渡します。メソッド内で、ポイント先オブジェクトのプロパティを 10 に設定します。メソッドでは再度属性を出力していますが、この時の出力結果は10です。MyClass
obj
value
foo
obj
foo
obj
value
main
obj
value
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.setValue(5);
System.out.println(obj.getValue()); // 输出5
foo(obj);
System.out.println(obj.getValue()); // 输出5
}
public static void foo(MyClass obj) {
obj = new MyClass();
obj.setValue(10);
}
MyClass
このコードでは、整数プロパティを含むクラス を定義し value
、そのプロパティのメソッドにアクセスします。このメソッドでは 、オブジェクト main
を作成し 、 そのプロパティを 5 に設定します。次に、 というメソッドを呼び出し 、 これをパラメータとして渡します。メソッド内で 、obj を新しい参照に割り当て、次に新しい参照 obj に値を割り当てます。この時点では、実際には 2 つのオブジェクトがあります。foo が呼び出された後、そのスタック フレームは破棄されるため、元のオブジェクトは変更されず、5 が出力されます。MyClass
obj
value
foo
obj
foo
4. まとめ
面接では、スタックとヒープの概念と基本構造を覚えておく必要があります。
スタック | ヒープ |
---|---|
スレッドプライベートデータを保存する | 共有データを保存する |
配列またはリンク リストの実装 | ツリー構造 |
比較的高速なアクセス | アクセスが比較的遅い |
また、Java では、基本データ型であってもカスタム オブジェクトであっても、パラメータの受け渡しは値の受け渡しであることに注意してください。これは、カスタム オブジェクトをパラメータとしてメソッドに渡すとき、実際に渡されるのは参照のコピーであることを意味します。メソッドが参照によって指されるオブジェクトのプロパティを内部的に変更すると、元の参照によって指されるオブジェクトに影響します。ただし、メソッドが内部で新しいオブジェクトへの参照を指している場合、元の参照が指しているオブジェクトには影響しません。