Apakah inisialisasi objek di Java “Foo f = new Foo ()” pada dasarnya sama dengan menggunakan malloc untuk sebuah pointer di C?

9

Saya mencoba memahami proses aktual di balik kreasi objek di Jawa - dan saya kira bahasa pemrograman lain.

Apakah salah untuk menganggap bahwa inisialisasi objek di Jawa sama dengan ketika Anda menggunakan malloc untuk struktur di C?

Contoh:

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

Apakah ini sebabnya objek dikatakan berada di heap daripada stack? Karena mereka pada dasarnya hanya petunjuk data?

Jules
sumber
Objek dibuat di heap untuk bahasa yang dikelola seperti c # / java. Di cpp Anda juga dapat membuat objek di stack
bas
Mengapa pencipta Java / C # memutuskan untuk secara eksklusif menyimpan objek di heap?
Jules
Saya pikir demi kesederhanaan. Menyimpan objek pada stack dan memberikannya level yang lebih dalam melibatkan penyalinan objek pada stack, yang melibatkan copy-constructor. Saya tidak mencari jawaban yang benar di Google, tetapi saya yakin Anda dapat menemukan jawaban yang lebih memuaskan sendiri (atau orang lain akan menguraikan pertanyaan sampingan ini)
bas
@Jules objek di java masih bisa "didekompresi" pada saat run-time (dipanggil scalar-replacement) menjadi bidang-bidang yang hanya tinggal di stack saja; tapi itu adalah sesuatu yang JITtidak, tidak javac.
Eugene
"Heap" hanyalah nama untuk sekumpulan properti yang terkait dengan objek / memori yang dialokasikan. Dalam C / C ++ Anda dapat memilih dari dua set properti yang berbeda, yang disebut "tumpukan" dan "tumpukan", di C # dan Java, semua alokasi objek memiliki perilaku yang sama, yang berjalan di bawah nama "tumpukan", yang tidak menyiratkan bahwa properti ini sama dengan "tumpukan" C / C ++, pada kenyataannya, mereka tidak. Ini tidak berarti bahwa implementasi tidak dapat memiliki strategi yang berbeda untuk mengelola objek, itu menyiratkan bahwa strategi itu tidak relevan dengan logika aplikasi.
Holger

Jawaban:

5

Di C, malloc()mengalokasikan wilayah memori di heap dan mengembalikan pointer ke sana. Hanya itu yang Anda dapatkan. Memori tidak diinisialisasi dan Anda tidak memiliki jaminan bahwa semuanya nol atau apa pun.

Di Jawa, panggilan newtidak seperti alokasi heap malloc(), tetapi Anda juga mendapatkan banyak kenyamanan tambahan (atau overhead, jika Anda mau). Misalnya, Anda tidak harus secara eksplisit menentukan jumlah byte yang akan dialokasikan. Compiler menghitungnya untuk Anda berdasarkan pada jenis objek yang Anda coba alokasikan. Selain itu, konstruktor objek dipanggil (yang dapat Anda berikan argumen jika Anda ingin mengontrol bagaimana inisialisasi terjadi). Ketika newkembali, Anda dijamin memiliki objek yang diinisialisasi.

Tapi ya, pada akhir panggilan baik hasil malloc()dan newhanya petunjuk ke beberapa data berbasis heap.

Bagian kedua dari pertanyaan Anda menanyakan tentang perbedaan antara tumpukan dan tumpukan. Jawaban yang jauh lebih komprehensif dapat ditemukan dengan mengikuti kursus tentang (atau membaca buku tentang) desain kompiler. Kursus tentang sistem operasi juga akan sangat membantu. Ada juga banyak pertanyaan dan jawaban di SO tentang tumpukan dan tumpukan.

Karena itu, saya akan memberikan gambaran umum saya harap tidak terlalu bertele-tele dan bertujuan untuk menjelaskan perbedaan pada tingkat yang cukup tinggi.

Pada dasarnya, alasan utama untuk memiliki dua sistem manajemen memori, yaitu tumpukan dan tumpukan, adalah untuk efisiensi . Alasan kedua adalah bahwa masing-masing lebih baik pada jenis masalah tertentu daripada yang lain.

Tumpukan agak lebih mudah bagi saya untuk dipahami sebagai konsep, jadi saya mulai dengan tumpukan. Mari kita pertimbangkan fungsi ini di C ...

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

Di atas tampaknya cukup mudah. Kami mendefinisikan fungsi bernama add()dan meneruskan di addend kiri dan kanan. Fungsi menambahkannya dan mengembalikan hasilnya. Harap abaikan semua hal tepi-kasus seperti luapan yang mungkin terjadi, pada titik ini tidak berhubungan dengan diskusi.

Tujuan add()fungsi ini tampaknya cukup mudah, tetapi apa yang bisa kita katakan tentang siklus hidupnya? Terutama kebutuhan pemanfaatan memorinya?

Yang paling penting, kompiler mengetahui apriori (yaitu pada waktu kompilasi) seberapa besar tipe data dan berapa banyak yang akan digunakan. The lhsdan rhsargumen yang sizeof(int), 4 byte masing-masing. Variabelnya resultjuga sizeof(int). Kompiler dapat mengetahui bahwa add()fungsi tersebut menggunakan 4 bytes * 3 intsatau total 12 byte memori.

Ketika add()fungsi dipanggil, register perangkat keras yang disebut penunjuk tumpukan akan memiliki alamat di dalamnya yang menunjuk ke bagian atas tumpukan. Untuk mengalokasikan memori yang add()harus dijalankan fungsi, semua kode fungsi-entri perlu lakukan adalah mengeluarkan satu instruksi bahasa rakitan tunggal untuk mengurangi nilai register penunjuk tumpukan dengan 12. Dengan demikian, ia menciptakan penyimpanan pada tumpukan selama tiga ints, masing-masing untuk lhs, rhs, dan result. Mendapatkan ruang memori yang Anda butuhkan dengan mengeksekusi instruksi tunggal adalah kemenangan besar dalam hal kecepatan karena instruksi tunggal cenderung dieksekusi dalam satu clock tick (1 milyar detik, 1 CPU 1 GHz).

Selain itu, dari tampilan kompiler, ia dapat membuat peta ke variabel yang terlihat sangat buruk seperti mengindeks array:

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

Sekali lagi, semua ini sangat cepat.

Ketika add()fungsi keluar harus dibersihkan. Ini dilakukan dengan mengurangi 12 byte dari register penunjuk tumpukan. Ini mirip dengan panggilan untuk free()tetapi hanya menggunakan satu instruksi CPU dan hanya membutuhkan satu centang. Ini sangat, sangat cepat.


Sekarang pertimbangkan alokasi berbasis heap. Ini berlaku ketika kita tidak tahu apriori berapa banyak memori yang akan kita butuhkan (yaitu kita hanya akan mempelajarinya saat runtime).

Pertimbangkan fungsi ini:

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;
}

Perhatikan bahwa addRandom()fungsi tidak tahu pada waktu kompilasi berapa nilai countargumen itu. Karena itu, tidak masuk akal untuk mencoba mendefinisikan arrayseperti yang kita lakukan jika kita meletakkannya di tumpukan, seperti ini:

int array[count];

Jika countbesar, ini dapat menyebabkan tumpukan kami tumbuh terlalu besar dan menimpa segmen program lainnya. Ketika stack overflow ini terjadi, program Anda mogok (atau lebih buruk).

Jadi, dalam kasus di mana kita tidak tahu berapa banyak memori yang akan kita butuhkan sampai runtime, kita gunakan malloc(). Kemudian kita bisa menanyakan jumlah byte yang kita butuhkan saat kita membutuhkannya, dan malloc()akan memeriksa apakah bisa byte sebanyak itu. Jika bisa, bagus, kami mendapatkannya kembali, jika tidak, kami mendapatkan pointer NULL yang memberi tahu kami bahwa panggilan malloc()gagal. Khususnya, program ini tidak macet! Tentu saja Anda sebagai programmer dapat memutuskan bahwa program Anda tidak diizinkan berjalan jika alokasi sumber daya gagal, tetapi pemutusan yang diprogram oleh programmer berbeda dari crash palsu.

Jadi sekarang kita harus kembali untuk melihat efisiensi. Penyusun stack sangat cepat - satu instruksi untuk dialokasikan, satu instruksi untuk membatalkan alokasi, dan itu dilakukan oleh kompiler, tetapi ingat stack dimaksudkan untuk hal-hal seperti variabel lokal dari ukuran yang diketahui sehingga cenderung cukup kecil.

Pengalokasi tumpukan di sisi lain adalah beberapa pesanan lebih lambat lebih besar. Itu harus melakukan pencarian di tabel untuk melihat apakah ia memiliki cukup memori bebas untuk dapat memv jumlah memori yang diinginkan pengguna. Itu harus memperbarui tabel-tabel setelah vending memori untuk memastikan tidak ada orang lain yang dapat menggunakan blok itu (pembukuan ini mungkin memerlukan pengalokasi untuk mencadangkan memori untuk dirinya sendiri di samping apa yang ia rencanakan untuk dijual). Pengalokasi harus menggunakan strategi penguncian untuk memastikannya mengosongkan memori dengan cara yang aman. Dan ketika ingatan akhirnyafree()d, yang terjadi pada waktu yang berbeda dan tanpa urutan yang dapat diprediksi biasanya, pengalokasi harus menemukan blok yang berdekatan dan menyatukannya kembali untuk memperbaiki fragmentasi timbunan. Jika kedengarannya seperti itu akan membutuhkan lebih dari satu instruksi CPU tunggal untuk menyelesaikan semua itu, Anda benar! Ini sangat rumit dan butuh beberapa saat.

Tapi tumpukan itu besar. Jauh lebih besar dari tumpukan. Kita bisa mendapatkan banyak memori dari mereka dan mereka hebat ketika kita tidak tahu pada waktu kompilasi berapa banyak memori yang kita butuhkan. Jadi, kami menukar kecepatan dengan sistem memori terkelola yang menolak kami dengan sopan alih-alih menabrak ketika kami mencoba mengalokasikan sesuatu yang terlalu besar.

Saya harap itu membantu menjawab beberapa pertanyaan Anda. Harap beri tahu saya jika Anda ingin klarifikasi pada salah satu di atas.

par
sumber
intbukan 8 byte pada platform 64-bit. Masih 4. Seiring dengan itu, kompiler sangat mungkin untuk mengoptimalkan ketiga intdari tumpukan ke register kembali. Faktanya, dua argumen juga kemungkinan ada dalam register pada platform 64-bit.
SS Anne
Saya telah mengedit jawaban saya untuk menghapus pernyataan tentang 8-byte intpada platform 64-bit. Anda benar yang inttetap 4-byte di Jawa. Saya telah meninggalkan sisa jawaban saya namun karena saya percaya masuk ke optimasi kompiler menempatkan kereta di depan kuda. Ya, Anda juga benar pada poin-poin ini, tetapi pertanyaannya meminta klarifikasi tentang tumpukan vs tumpukan. RVO, argumen lewat register, kode elision, dll membebani konsep dasar dan menghalangi pemahaman dasar-dasar.
par