Javaのオブジェクト初期化“ Foo f = new Foo()”は、Cのポインターにmallocを使用することと本質的に同じですか?


9

Javaでのオブジェクト作成の背後にある実際のプロセスを理解しようとしています。他のプログラミング言語もあると思います。

Javaでのオブジェクトの初期化がCで構造体にmallocを使用する場合と同じであると想定するのは間違っているでしょうか?

例:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

これが、オブジェクトがスタックではなくヒープ上にあると言われるのはなぜですか?それらは本質的にデータへの単なるポインタなので?


オブジェクトは、c#/ javaなどのマネージ言語のヒープ上に作成されます。cppでは、スタック上にオブジェクトを作成することもできます
bas

Java / C#の作成者がオブジェクトをヒープに排他的に格納することにしたのはなぜですか?
ジュール・

簡単にするために思います。オブジェクトをスタックに格納し、さらに深いレベルに渡すには、オブジェクトをスタックにコピーする必要があります。これには、コピーコンストラクターが含まれます。私は正しい答えを探してググりませんでしたが、自分でもっと満足できる答えを見つけることができると確信しています(または誰か他の誰かがこの副次的な質問について詳しく説明します)
bas

Javaの@Julesオブジェクトは、実行時scalar-replacementにスタックにのみ存在するプレーンフィールドだけに「デコンパック」(と呼ばれます)できます。しかし、それはできることでJITあり、そうではありませんjavac
ユージーン

「ヒープ」は、割り当てられたオブジェクト/メモリに関連付けられた一連のプロパティの単なる名前です。C / C ++では、「スタック」と「ヒープ」と呼ばれる2つの異なるプロパティセットから選択できます。C#とJavaでは、すべてのオブジェクト割り当てに同じ指定の動作があり、「ヒープ」という名前ではありません。これらのプロパティはC / C ++の「ヒープ」と同じであることを意味しますが、実際は同じではありません。これは、実装がオブジェクトを管理するための異なる戦略を持つことができないことを意味するのではなく、それらの戦略がアプリケーションロジックに無関係であることを意味します。
Holger、

回答:


5

Cではmalloc()、ヒープ内のメモリ領域を割り当て、その領域へのポインタを返します。それだけです。メモリは初期化されておらず、すべてがゼロまたはその他であるという保証はありません。

Javaでは、呼び出しnewはと同じようにヒープベースの割り当てを行いmalloc()ますが、さらに多くの便利さ(または必要に応じてオーバーヘッド)も得られます。たとえば、割り当てるバイト数を明示的に指定する必要はありません。コンパイラーは、割り当てようとしているオブジェクトのタイプに基づいてそれを計算します。さらに、オブジェクトコンストラクターが呼び出されます(初期化の方法を制御する場合は、引数を渡すことができます)。new戻ったとき、初期化されたオブジェクトがあることが保証されます。

しかし、はい、呼び出しの終わりに、の結果malloc()newヒープベースのデータのチャンクへの単なるポインタの両方です。

質問の後半では、スタックとヒープの違いについて質問します。コンパイラ設計に関するコースを受講する(または本に関する本を読む)ことで、はるかに包括的な答えを見つけることができます。オペレーティングシステムに関するコースも役立ちます。スタックとヒープに関するSOに関する質問と回答も数多くあります。

そうは言っても、私は、概要が多すぎないことを望み、違いをかなり高いレベルで説明することを目的とした一般的な概要を説明します。

基本的に、ヒープとスタックの2つのメモリ管理システムを使用する主な理由は、効率のためです。二次的な理由は、それぞれが特定の種類の問題で他よりも優れていることです。

スタックは概念として理解するのが少し簡単なので、スタックから始めます。この関数をCで考えてみましょう...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

上記はかなり簡単なようです。名前付きの関数を定義し、add()左右の加数で渡します。関数はそれらを追加し、結果を返します。発生する可能性のあるオーバーフローなど、すべてのエッジケースのものを無視してください。この時点では、議論に密接な関係はありません。

add()機能の目的は、かなり簡単だが、私たちは、そのライフサイクルについて何言うことができますか?特にそのメモリ使用量は必要ですか?

最も重要なのは、コンパイラーがデータ型の大きさと使用される数をアプリオリに(つまり、コンパイル時に)知っていることです。lhsそしてrhs引数はsizeof(int)、各バイト4。変数resultsizeof(int)です。コンパイラーは、add()関数が4 bytes * 3 ints合計12バイトのメモリーを使用していることを通知できます。

ときにadd()関数が呼び出され、ハードウェアレジスタと呼ばれるスタックポインタがスタックの先頭を指していること、それにアドレスを持つことになります。add()関数が実行する必要のあるメモリを割り当てるために、関数エントリコードが実行する必要があるすべてのことは、1つのアセンブリ言語命令を発行してスタックポインタレジスタ値を12だけデクリメントすることです。これにより、スタックに3つのストレージが作成されます。ints1つずつについてlhsrhs、とresult。単一の命令は1クロックティック(10億分の1秒、1 GHz CPU)で実行される傾向があるため、単一の命令を実行して必要なメモリ領域を取得することは、速度の面で大きな利点です。

また、コンパイラーの観点からは、配列のインデックス付けに非常によく似た変数へのマップを作成できます。

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

繰り返しますが、これはすべて非常に高速です。

ときにadd()関数が終了し、それはきれいにしています。これは、スタックポインタレジスタから12バイトを減算することによって行われます。これはへの呼び出しに似ていますがfree()、CPU命令を1つだけ使用し、ティックを1つだけ使用します。非常に高速です。


次に、ヒープベースの割り当てを考えます。これは、必要なメモリ容量がアプリオリにわからない場合に関係します(つまり、実行時にのみそれについて学習します)。

この関数を考えてみましょう:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

addRandom()関数はコンパイル時にcount引数の値がどうなるかを認識していないことに注意してください。このためarray、次のようにスタックに配置する場合のように定義しようとしても意味がありません。

int array[count];

Ifはcount巨大であり、それは、私たちのスタックが大きすぎる成長し、他のプログラム・セグメントを上書きする可能性があります。このスタックオーバーフローが発生すると、プログラムがクラッシュします(さらに悪化します)。

したがって、実行時までに必要なメモリの量がわからない場合は、を使用しますmalloc()。次に、必要なときに必要なバイト数を要求し、malloc()それだけ多くのバイト数を提供できるかどうかを確認します。可能であれば、それを元に戻し、できなければ、呼び出しがmalloc()失敗したことを伝えるNULLポインターを取得します。特に、プログラムはクラッシュしません!もちろん、プログラマーであるあなたは、リソース割り当てが失敗した場合、プログラムの実行を許可しないことを決定できますが、プログラマーが開始した終了は、偽のクラッシュとは異なります。

ですから、効率性を見るために戻ってくる必要があります。スタックアロケーターは非常に高速です-割り当てる1つの命令、割り当てを解除する1つの命令、そしてコンパイラーによって実行されますが、スタックは既知のサイズのローカル変数のようなもののために意図されているため、かなり小さい傾向があります。

一方、ヒープアロケータは数桁遅くなります。テーブルを検索して、ユーザーが必要とするメモリ量を販売できる十分な空きメモリがあるかどうかを確認する必要があります。それは確か何も他のものを作るためにメモリを販売するん後にそれは(この簿記は、予備のメモリへの割り当て必要になることがあり、そのブロックを使用することができ、それらのテーブルを更新する必要があり、それ自体のために、それは、販売することを計画するものに加えて)。アロケータは、スレッドセーフな方法でメモリを確実に提供するために、ロック戦略を採用する必要があります。そして記憶がついにfree()dは、異なる時間に発生し、通常は予測可能な順序ではありません。ヒープの断片化を修復するために、アロケーターは連続したブロックを見つけてそれらをつなぎ合わせる必要があります。それを実現するために1つ以上のCPU命令を必要とするように思える場合、そのとおりです。非常に複雑で時間がかかります。

しかし、ヒープは大きいです。スタックよりもはるかに大きい。それらから多くのメモリを取得することができ、コンパイル時に必要なメモリ量がわからない場合に最適です。したがって、大きすぎるものを割り当てようとするとクラッシュするのではなく、速度を落としてマネージメモリシステムを低下させます。

それがあなたの質問のいくつかに答えるのに役立つことを願っています。上記のいずれかについてご不明な点がありましたらお知らせください。


int64ビットプラットフォームでは8バイトではありません。それはまだ4です。それに加えて、コンパイラーはintスタックの3番目を戻りレジスターに最適化する可能性が非常に高いです。実際、2つの引数は、64ビットプラットフォームのレジスタにも存在する可能性があります。
SSアン

回答を編集して、int64ビットプラットフォームでの8バイトsに関する記述を削除しました。そのあなたは正しいintJavaで4バイトのまま。しかし、コンパイラーの最適化に入るとカートが馬の前に置かれると思うので、残りの回答は残しておきます。はい、あなたもこれらの点で正しいですが、質問はスタックとヒープの明確化を求めています。RVO、レジスタを介した引数の受け渡し、コードの省略などは、基本的な概念に過度の負担をかけ、基本を理解するのに邪魔になります。
パー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.