Array, heap dan stack dan tipe nilai

134
int[] myIntegers;
myIntegers = new int[100];

Dalam kode di atas, apakah int baru [100] menghasilkan array pada heap? Dari apa yang saya baca di CLR via c #, jawabannya adalah ya. Tapi yang tidak bisa saya mengerti adalah apa yang terjadi pada int sebenarnya di dalam array. Karena mereka adalah tipe nilai, saya kira mereka harus dikotak, karena saya dapat, misalnya, meneruskan myIntegers ke bagian lain dari program dan itu akan mengacaukan tumpukan jika mereka dibiarkan di atasnya sepanjang waktu . Atau saya salah? Saya kira mereka hanya akan kotak dan akan hidup di tumpukan selama array ada.

melahap elysium
sumber

Jawaban:

289

Array Anda dialokasikan pada heap, dan int tidak kotak.

Sumber kebingungan Anda kemungkinan karena orang mengatakan bahwa tipe referensi dialokasikan pada heap, dan tipe nilai dialokasikan pada tumpukan. Ini bukan representasi yang sepenuhnya akurat.

Semua variabel dan parameter lokal dialokasikan pada tumpukan. Ini termasuk tipe nilai dan tipe referensi. Perbedaan antara keduanya hanyalah apa yang disimpan dalam variabel. Tidak mengherankan, untuk tipe nilai, nilai tipe disimpan langsung dalam variabel, dan untuk tipe referensi, nilai tipe disimpan di heap, dan referensi ke nilai ini adalah apa yang disimpan dalam variabel.

Hal yang sama berlaku untuk bidang. Ketika memori dialokasikan untuk instance dari tipe agregat (a classatau astruct ), itu harus menyertakan penyimpanan untuk masing-masing bidang instance. Untuk bidang tipe referensi, penyimpanan ini hanya menyimpan referensi ke nilai, yang akan dialokasikan pada tumpukan nanti. Untuk bidang tipe nilai, penyimpanan ini menyimpan nilai aktual.

Jadi, diberikan jenis berikut:

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}

Nilai dari masing-masing jenis ini akan membutuhkan 16 byte memori (dengan asumsi ukuran kata 32-bit). Bidang Idalam setiap kasus membutuhkan 4 byte untuk menyimpan nilainya, bidang Smembutuhkan 4 byte untuk menyimpan referensi, dan bidang Lmembutuhkan 8 byte untuk menyimpan nilainya. Jadi memori untuk nilai keduanya RefTypedan ValTypeterlihat seperti ini:

 0 ┌──────────────────┐
   │ I │
 4 ├──────────────────┤
   │ S │
 8 ├──────────────────┤
   │ L │
   │ │
16 └──────────────────┘

Sekarang jika Anda memiliki tiga variabel lokal dalam suatu fungsi, jenis RefType, ValTypedan int[], seperti ini:

RefType refType;
ValType valType;
int[]   intArray;

maka tumpukan Anda mungkin terlihat seperti ini:

 0 ┌──────────────────┐
   │ refType │
 4 ├──────────────────┤
   │ valType │
   │ │
   │ │
   │ │
20 ├──────────────────┤
   │ intArray │
24 └──────────────────┘

Jika Anda menetapkan nilai ke variabel lokal ini, seperti:

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

Maka tumpukan Anda mungkin terlihat seperti ini:

 0 ┌──────────────────┐
   │ 0x4A963B68 │ - heap address `refType`
 4 ├──────────────────┤
   │ 200 │ - nilai `valType.I`
   │ 0x4A984C10 │ - tumpukan alamat `valType.S`
   │ 0x44556677 │ - `valType.L` 32-bit yang rendah
   │ 0x00112233 │ - `valType.L` 32-bit yang tinggi
20 ├──────────────────┤
   │ 0x4AA4C288 │ - heap address `intArray`
24 └──────────────────┘

Memori di alamat 0x4A963B68(nilai refType) akan menjadi seperti:

 0 ┌──────────────────┐
   │ 100 │ - nilai `refType.I`
 4 ├──────────────────┤
   │ 0x4A984D88 │ - heap address `refType.S`
 8 ├──────────────────┤
   │ 0x89ABCDEF │ - `refType.L` 32-bit yang rendah
   │ 0x01234567 │ - `refType.L` 32-bit yang tinggi
16 └──────────────────┘

Memori di alamat 0x4AA4C288(nilai intArray) akan menjadi seperti:

 0 ┌──────────────────┐
   │ 4 │ - panjang array
 4 ├──────────────────┤
   │ 300 │ - `intArray [0]`
 8 ├──────────────────┤
   │ 301 │ - `intArray [1]`
12 ├──────────────────┤
   │ 302 │ - `intArray [2]`
16 ├──────────────────┤
   │ 303 │ - `intArray [3]`
20 └──────────────────┘

Sekarang, jika Anda beralih intArrayke fungsi lain, nilai yang didorong ke stack akan menjadi 0x4AA4C288, alamat array, bukan salinan array.

P Ayah
sumber
52
Saya perhatikan bahwa pernyataan bahwa semua variabel lokal disimpan di stack tidak akurat. Variabel lokal yang merupakan variabel luar dari fungsi anonim disimpan di heap. Variabel lokal dari blok iterator disimpan di heap. Variabel lokal blok async disimpan di heap. Variabel lokal yang didaftarkan disimpan pada stack atau heap. Variabel lokal yang elided disimpan pada stack atau heap.
Eric Lippert
5
LOL, selalu pemetik nit, Tuan Lippert. :) Saya merasa terdorong untuk menunjukkan bahwa dengan pengecualian pada dua kasus terakhir Anda, apa yang disebut "penduduk setempat" tidak lagi menjadi penduduk setempat pada waktu kompilasi. Implementasi mengangkat mereka ke status anggota kelas, yang merupakan satu-satunya alasan mereka disimpan di heap. Jadi itu hanyalah detail implementasi (kekek). Tentu saja, penyimpanan register adalah detail implementasi level yang lebih rendah, dan elision tidak masuk hitungan.
P Ayah
3
Tentu saja, seluruh posting saya adalah detail implementasi, tetapi, seperti yang saya yakin Anda sadari, itu semua dalam upaya untuk memisahkan konsep variabel dan nilai . Variabel (sebut saja lokal, bidang, parameter, apa pun) dapat disimpan di tumpukan, tumpukan, atau tempat lain yang ditentukan implementasi, tapi itu tidak terlalu penting. Yang penting, adalah apakah variabel itu secara langsung menyimpan nilai yang diwakilinya, atau hanya referensi ke nilai itu, disimpan di tempat lain. Ini penting karena mempengaruhi semantik salinan: apakah menyalin variabel itu menyalin nilainya atau alamatnya.
P Ayah
16
Tampaknya Anda memiliki gagasan berbeda tentang apa artinya menjadi "variabel lokal" daripada saya. Anda tampaknya percaya bahwa "variabel lokal" ditandai oleh detail implementasinya . Keyakinan ini tidak dibenarkan oleh apa pun yang saya ketahui dalam spesifikasi C #. Variabel lokal sebenarnya adalah variabel yang dideklarasikan di dalam blok yang namanya berada dalam lingkup hanya di seluruh ruang deklarasi yang terkait dengan blok. Saya meyakinkan Anda bahwa variabel lokal yang, sebagai detail implementasi, diangkat ke bidang kelas penutupan, masih variabel lokal sesuai dengan aturan C #.
Eric Lippert
15
Yang mengatakan, tentu saja jawaban Anda umumnya sangat baik; titik bahwa nilai - nilai secara konseptual berbeda dari variabel adalah sesuatu yang perlu dibuat sesering dan sekeras mungkin, karena itu fundamental. Namun banyak sekali orang yang percaya mitos paling aneh tentang mereka! Begitu baik untuk Anda karena berjuang dalam pertarungan yang baik.
Eric Lippert
23

Ya array akan ditemukan di heap.

Int di dalam array tidak akan dikotak. Hanya karena tipe nilai ada di heap, tidak berarti itu akan kotak. Boxing hanya akan terjadi ketika tipe nilai, seperti int, ditugaskan untuk referensi objek tipe.

Sebagai contoh

Tidak kotak:

int i = 42;
myIntegers[0] = 42;

Kotak:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

Anda mungkin juga ingin melihat posting Eric tentang hal ini:

JaredPar
sumber
1
Tapi saya tidak mengerti. Bukankah seharusnya nilai jenis dialokasikan pada tumpukan? Atau kedua nilai dan tipe referensi dapat dialokasikan baik di heap atau stack dan hanya saja mereka biasanya disimpan di satu tempat atau yang lain?
melahap elysium
4
@Jorge, tipe nilai tanpa pembungkus / wadah tipe referensi akan hidup di stack. Namun begitu itu digunakan dalam wadah jenis referensi itu akan hidup di tumpukan. Array adalah tipe referensi dan karenanya memori untuk int harus ada di heap.
JaredPar
2
@ Jorge: tipe referensi hanya hidup di heap, tidak pernah di stack. Sebaliknya, tidak mungkin (dalam kode yang dapat diverifikasi) untuk menyimpan pointer ke lokasi stack ke objek tipe referensi.
Anton Tykhyy
1
Saya pikir Anda bermaksud menugaskan saya untuk arr [0]. Konstanta tugas masih akan menyebabkan tinju dari "42", tapi Anda buat saya, sehingga Anda mungkin juga menggunakannya ;-)
Marcus Griep
@AntonTykhyy: Tidak ada aturan yang saya tahu mengatakan CLR tidak dapat melakukan analisis melarikan diri. Jika mendeteksi bahwa suatu objek tidak akan pernah direferensikan melewati masa hidup fungsi yang membuatnya, itu sepenuhnya sah - dan bahkan lebih baik - untuk membangun objek pada stack, apakah itu tipe nilai atau bukan. "Tipe nilai" dan "tipe referensi" pada dasarnya menggambarkan apa yang ada di memori yang diambil oleh variabel, bukan aturan yang keras dan cepat di mana objek tinggal.
cao
21

Untuk memahami apa yang terjadi, berikut adalah beberapa fakta:

  • Objek selalu dialokasikan di heap.
  • Tumpukan hanya berisi objek.
  • Jenis nilai dialokasikan di tumpukan, atau bagian dari objek di tumpukan.
  • Array adalah objek.
  • Array hanya dapat berisi tipe nilai.
  • Referensi objek adalah tipe nilai.

Jadi, jika Anda memiliki array bilangan bulat, array dialokasikan pada heap dan bilangan bulat yang dikandungnya adalah bagian dari objek array di heap. Bilangan bulat berada di dalam objek array di heap, bukan sebagai objek terpisah, sehingga tidak kotak.

Jika Anda memiliki array string, itu benar-benar array referensi string. Karena referensi adalah tipe nilai, mereka akan menjadi bagian dari objek array di heap. Jika Anda meletakkan objek string dalam array, Anda sebenarnya meletakkan referensi ke objek string dalam array, dan string adalah objek terpisah di heap.

Guffa
sumber
Ya, referensi berperilaku persis seperti tipe nilai tetapi saya perhatikan mereka biasanya tidak disebut seperti itu, atau termasuk dalam tipe nilai. Lihat misalnya (tetapi ada lebih banyak seperti ini) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman
@Enk: Ya, Anda benar bahwa referensi tidak terdaftar di antara variabel tipe nilai, tetapi ketika sampai pada bagaimana memori dialokasikan untuk mereka, mereka ada di setiap jenis nilai hormat, dan itu sangat berguna untuk menyadari bahwa untuk memahami bagaimana alokasi memori semua cocok bersama. :)
Guffa
Saya ragu dengan poin ke-5, "Array hanya dapat berisi tipe nilai." Bagaimana dengan array string? string [] string = string baru [4];
Sunil Purushothaman
9

Saya pikir inti dari pertanyaan Anda terletak pada kesalahpahaman tentang jenis referensi dan nilai. Ini adalah sesuatu yang mungkin diperjuangkan oleh setiap pengembang .NET dan Java.

Array hanyalah daftar nilai. Jika itu adalah array dari tipe referensi (katakanlah a string[]) maka array adalah daftar referensi ke berbagai stringobjek di heap, karena referensi adalah nilai dari tipe referensi. Secara internal, referensi ini diimplementasikan sebagai pointer ke alamat dalam memori. Jika Anda ingin memvisualisasikan ini, array seperti itu akan terlihat seperti ini di memori (di heap):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Ini adalah array stringyang berisi 4 referensi ke stringobjek di heap (angka-angka di sini adalah heksadesimal). Saat ini, hanya yang terakhir yang stringbenar-benar menunjuk ke sesuatu (memori diinisialisasi ke nol ketika dialokasikan), array ini pada dasarnya akan menjadi hasil dari kode ini di C #:

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

Array di atas akan dalam program 32 bit. Dalam program 64 bit, referensi akan dua kali lebih besar ( F8AB56AAakan 00000000F8AB56AA).

Jika Anda memiliki sebuah array jenis nilai (mengatakan sebuah int[]) maka array adalah daftar bilangan bulat, sebagai nilai dari jenis nilai adalah nilai itu sendiri (maka nama). Visualisasi array seperti ini adalah sebagai berikut:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Ini adalah array dari 4 bilangan bulat, di mana hanya int kedua diberikan nilai (ke 1174352571, yang merupakan representasi desimal dari angka heksadesimal) dan sisa bilangan bulat akan menjadi 0 (seperti yang saya katakan, memori diinisialisasi ke nol dan 00000000 dalam heksadesimal adalah 0 dalam desimal). Kode yang menghasilkan array ini adalah:

 int[] integers = new int[4];
 integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too

int[]Array ini juga akan disimpan di heap.

Sebagai contoh lain, memori short[4]array akan terlihat seperti ini:

[ 0000, 0000, 0000, 0000 ]

Karena nilai a shortadalah angka 2 byte.

Di mana tipe nilai disimpan, hanyalah detail implementasi seperti yang dijelaskan Eric Lippert dengan sangat baik di sini , tidak melekat pada perbedaan antara nilai dan tipe referensi (yang merupakan perbedaan perilaku).

Ketika Anda melewati sesuatu untuk metode (bahwa tipe referensi atau tipe nilai) kemudian copy dari nilai dari jenis yang benar-benar melewati ke metode. Dalam kasus tipe referensi, nilainya adalah referensi (anggap ini sebagai penunjuk ke sepotong memori, meskipun itu juga merupakan detail implementasi) dan dalam kasus tipe nilai, nilainya adalah benda itu sendiri.

// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}

Boxing hanya terjadi jika Anda mengonversi tipe nilai ke tipe referensi. Kotak kode ini:

object o = 5;
JulianR
sumber
Saya percaya "detail implementasi" harus berukuran font: 50px. ;)
sisve
2

Ini adalah ilustrasi yang menggambarkan jawaban di atas oleh @P Daddy

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Dan saya menggambarkan konten yang sesuai dengan gaya saya.

masukkan deskripsi gambar di sini

Taman YoungMin
sumber
@ P Ayah saya membuat ilustrasi. Silakan periksa apakah ada bagian yang salah. Dan saya punya beberapa pertanyaan tambahan. 1. Ketika saya membuat 4 tipe array panjang, informasi panjang (4) juga selalu disimpan dalam memori?
YoungMin Park
2. Pada ilustrasi kedua, alamat array yang disalin disimpan di mana? Apakah ini area tumpukan yang sama dengan alamat intArray disimpan? Apakah itu tumpukan lain tetapi jenis tumpukan yang sama? Apakah tumpukannya berbeda? 3. Apa artinya rendah 32-bit / tinggi 32-bit? 4. Apa yang mengembalikan nilai ketika saya mengalokasikan tipe nilai (dalam contoh ini, struktur) pada stack dengan menggunakan kata kunci baru? Apakah ini juga alamatnya? Ketika saya memeriksa dengan pernyataan ini Console.WriteLine (valType), itu akan menunjukkan nama yang sepenuhnya memenuhi syarat seperti objek seperti ConsoleApp.ValType.
YoungMin Park
5. valType.I = 200; Apakah pernyataan ini berarti saya mendapatkan alamat valType, dengan alamat ini saya mengakses ke I dan di sana saya menyimpan 200 tetapi "di stack".
YoungMin Park
1

Array bilangan bulat dialokasikan pada heap, tidak lebih, tidak kurang. referensi myIntegers ke awal bagian di mana int dialokasikan. Referensi itu terletak di tumpukan.

Jika Anda memiliki array objek tipe referensi, seperti tipe Object, myObjects [], yang terletak di stack, akan merujuk ke sekelompok nilai yang mereferensikan objek itu sendiri.

Untuk meringkas, jika Anda meneruskan myIntegers ke beberapa fungsi, Anda hanya meneruskan referensi ke tempat di mana kumpulan nyata bilangan bulat dialokasikan.

Dykam
sumber
1

Tidak ada tinju dalam kode contoh Anda.

Jenis nilai dapat hidup di heap seperti yang mereka lakukan di array int Anda. Array dialokasikan pada heap dan menyimpan int, yang kebetulan merupakan tipe nilai. Isi array diinisialisasi ke default (int), yang kebetulan nol.

Pertimbangkan kelas yang berisi tipe nilai:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Variabel h merujuk pada instance HasAnInt yang hidup di heap. Kebetulan mengandung tipe nilai. Tidak apa-apa, 'aku' kebetulan tinggal di tumpukan karena itu terkandung dalam kelas. Tidak ada tinju dalam contoh ini juga.

Curt Nichols
sumber
1

Cukup sudah dikatakan oleh semua orang, tetapi jika seseorang mencari sampel dan dokumentasi yang jelas (tetapi tidak resmi) tentang heap, stack, variabel lokal, dan variabel statis, lihat artikel lengkap Jon Skeet tentang Memori di .NET - apa yang terjadi dimana

Kutipan:

  1. Setiap variabel lokal (yaitu satu yang dideklarasikan dalam suatu metode) disimpan di stack. Itu termasuk variabel tipe referensi - variabel itu sendiri ada di stack, tetapi ingat bahwa nilai variabel tipe referensi hanya referensi (atau nol), bukan objek itu sendiri. Parameter metode juga dihitung sebagai variabel lokal, tetapi jika mereka dideklarasikan dengan pengubah ref, mereka tidak mendapatkan slotnya sendiri, tetapi berbagi slot dengan variabel yang digunakan dalam kode panggilan. Lihat artikel saya tentang parameter yang lewat untuk detail lebih lanjut.

  2. Variabel instan untuk tipe referensi selalu ada di heap. Di situlah objek itu sendiri "hidup".

  3. Variabel instan untuk tipe nilai disimpan dalam konteks yang sama dengan variabel yang mendeklarasikan tipe nilai. Slot memori untuk instance secara efektif berisi slot untuk setiap bidang dalam instance. Itu berarti (mengingat dua poin sebelumnya) bahwa variabel struct dideklarasikan dalam suatu metode akan selalu berada di stack, sedangkan variabel struct yang merupakan bidang contoh kelas akan berada di heap.

  4. Setiap variabel statis disimpan di heap, terlepas dari apakah itu dinyatakan dalam tipe referensi atau tipe nilai. Hanya ada satu slot total tidak peduli berapa banyak instance yang dibuat. (Tidak perlu ada instance yang dibuat untuk slot yang ada sekalipun.) Rincian yang tepat dari tumpukan variabel hidup rumit, tetapi dijelaskan secara rinci dalam artikel MSDN tentang subjek.

gmaran23
sumber