Matrizes, heap e tipos de pilha e valor

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

No código acima, o novo int [100] está gerando a matriz no heap? Pelo que li no CLR via c #, a resposta é sim. Mas o que não consigo entender é o que acontece com os int reais dentro da matriz. Como eles são tipos de valor, eu acho que eles teriam que ser encaixotados, como eu posso, por exemplo, passar meusIntegers para outras partes do programa e isso desorganizaria a pilha se eles ficassem nele o tempo todo . Ou eu estou errado? Eu acho que eles apenas estariam na caixa e viveriam na pilha enquanto a matriz existisse.

elísio devorado
fonte

Respostas:

289

Sua matriz está alocada no heap e as entradas não estão na caixa.

A origem da sua confusão é provável porque as pessoas disseram que os tipos de referência são alocados no heap e os tipos de valor são alocados na pilha. Esta não é uma representação totalmente precisa.

Todas as variáveis ​​e parâmetros locais são alocados na pilha. Isso inclui tipos de valor e tipos de referência. A diferença entre os dois é apenas o que é armazenado na variável. Não é novidade que, para um tipo de valor, o valor do tipo é armazenado diretamente na variável e, para um tipo de referência, o valor do tipo é armazenado no heap, e uma referência a esse valor é o que é armazenado na variável.

O mesmo vale para os campos. Quando a memória é alocada para uma instância de um tipo agregado (a classou a struct), ela deve incluir armazenamento para cada um de seus campos de instância. Para campos do tipo de referência, esse armazenamento mantém apenas uma referência ao valor, que seria alocado no heap posteriormente. Para campos do tipo valor, esse armazenamento retém o valor real.

Portanto, considerando os seguintes tipos:

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

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

Os valores de cada um desses tipos exigiriam 16 bytes de memória (assumindo um tamanho de palavra de 32 bits). O campo Iem cada caso leva 4 bytes para armazenar seu valor, o campo Sleva 4 bytes para armazenar sua referência e o campo Lleva 8 bytes para armazenar seu valor. Portanto, a memória para o valor de ambos RefTypee ValTypefica assim:

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

Agora, se você tivesse três variáveis locais em uma função, de tipos RefType, ValTypee int[], como este:

RefType refType;
ValType valType;
int[]   intArray;

sua pilha pode ficar assim:

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

Se você atribuiu valores a essas variáveis ​​locais, assim:

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;

Então sua pilha pode ficar assim:

 0 ┌───────────────────┐
   │ 0x4A963B68 │ - endereço de pilha de `refType`
 4 ├───────────────────┤
   │ 200 │ - valor de `valType.I`
   │ 0x4A984C10 │ - endereço de pilha de `valType.S`
   │ 0x44556677 │ - baixos 32 bits de `valType.L`
   │ 0x00112233 │ - alta de 32 bits de `valType.L`
20 ├───────────────────┤
   │ 0x4AA4C288 │ - endereço de pilha de `intArray`
24 └───────────────────┘

A memória no endereço 0x4A963B68(valor de refType) seria algo como:

 0 ┌───────────────────┐
   │ 100 │ - valor de `refType.I`
 4 ├───────────────────┤
   │ 0x4A984D88 │ - endereço de pilha de `refType.S`
 8 ├───────────────────┤
   │ 0x89ABCDEF │ - baixos 32 bits de `refType.L`
   │ 0x01234567 │ - alta de 32 bits de `refType.L`
16 └───────────────────┘

A memória no endereço 0x4AA4C288(valor de intArray) seria algo como:

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

Agora, se você passasse intArraypara outra função, o valor enviado para a pilha seria 0x4AA4C288o endereço da matriz, não uma cópia da matriz.

P Papai
fonte
52
Observo que a declaração de que todas as variáveis ​​locais estão armazenadas na pilha é imprecisa. Variáveis ​​locais que são variáveis ​​externas de uma função anônima são armazenadas no heap. Variáveis ​​locais dos blocos do iterador são armazenadas no heap. Variáveis ​​locais de blocos assíncronos são armazenadas no heap. Variáveis ​​locais registradas não são armazenadas na pilha nem na pilha. As variáveis ​​locais que são elididas não são armazenadas na pilha nem na pilha.
Eric Lippert
5
LOL, sempre quem escolhe, Sr. Lippert. :) Sinto-me compelido a ressaltar que, com exceção dos dois últimos casos, os chamados "locais" deixam de ser locais em tempo de compilação. A implementação os eleva ao status de membros da classe, que é o único motivo pelo qual eles são armazenados no heap. Portanto, é apenas um detalhe de implementação (risada). Obviamente, o armazenamento de registros é um detalhe de implementação de nível ainda mais baixo e a elision não conta.
P papai
3
Obviamente, todo o meu post são detalhes de implementação, mas, como tenho certeza, você percebeu que tudo estava tentando separar os conceitos de variáveis e valores . Uma variável (chame de local, campo, parâmetro, qualquer que seja) pode ser armazenada na pilha, na pilha ou em outro local definido pela implementação, mas isso não é realmente importante. O importante é se essa variável armazena diretamente o valor que representa, ou simplesmente uma referência a esse valor, armazenada em outro local. É importante porque afeta a semântica da cópia: se a cópia dessa variável copia seu valor ou seu endereço.
P papai
16
Aparentemente, você tem uma idéia diferente do que significa ser uma "variável local" do que eu. Você parece acreditar que uma "variável local" é caracterizada por seus detalhes de implementação . Essa crença não é justificada por nada que eu saiba na especificação C #. Uma variável local é de fato uma variável declarada dentro de um bloco cujo nome está no escopo apenas em todo o espaço de declaração associado ao bloco. Garanto que as variáveis ​​locais que, como um detalhe de implementação, são içadas nos campos de uma classe de fechamento, ainda são variáveis ​​locais de acordo com as regras do C #.
Eric Lippert
15
Dito isto, é claro que sua resposta geralmente é excelente; o ponto em que os valores são conceitualmente diferentes das variáveis é aquele que precisa ser feito com a maior frequência e o mais alto possível, pois é fundamental. E, no entanto, muitas pessoas acreditam nos mitos mais estranhos sobre eles! Tão bom em você por lutar a boa luta.
Eric Lippert
23

Sim, a matriz estará localizada na pilha.

As entradas dentro da matriz não serão colocadas em caixa. Só porque um tipo de valor existe na pilha, não significa necessariamente que ele será encaixotado. O boxe ocorrerá somente quando um tipo de valor, como int, for atribuído a uma referência do objeto de tipo.

Por exemplo

Não caixa:

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

Caixas:

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

Você também pode conferir a postagem de Eric sobre este assunto:

JaredPar
fonte
1
Mas eu não entendo. Os tipos de valor não devem ser alocados na pilha? Ou os tipos de valor e de referência podem ser alocados na pilha ou na pilha e é apenas que eles geralmente são armazenados em um local ou outro?
Elysium devorado
4
@Jorge, um tipo de valor sem invólucro / contêiner do tipo de referência ficará na pilha. No entanto, uma vez usado em um contêiner do tipo de referência, ele permanecerá na pilha. Uma matriz é um tipo de referência e, portanto, a memória para o int deve estar na pilha.
11119 JaredPar
2
@ Jorge: tipos de referência vivem apenas na pilha, nunca na pilha. Por outro lado, é impossível (em código verificável) armazenar um ponteiro para um local de pilha em um objeto de um tipo de referência.
Anton Tykhyy
1
Eu acho que você pretendia atribuir i a arr [0]. A atribuição constante vai ainda causa boxe de "42", mas você criou i, então você pode também usá-lo ;-)
Marcus Griep
@AntonTykhyy: Não tenho nenhuma regra de dizer que um CLR não pode fazer uma análise de escape. Se ele detectar que um objeto nunca será referenciado após a vida útil da função que o criou, é totalmente legítimo - e até preferível - construir o objeto na pilha, seja um tipo de valor ou não. "Tipo de valor" e "tipo de referência" basicamente descrevem o que há na memória ocupada pela variável, não uma regra rígida e rápida sobre onde o objeto mora.
Chao
21

Para entender o que está acontecendo, aqui estão alguns fatos:

  • Os objetos são sempre alocados no heap.
  • A pilha contém apenas objetos.
  • Os tipos de valor são alocados na pilha ou parte de um objeto no heap.
  • Uma matriz é um objeto.
  • Uma matriz pode conter apenas tipos de valor.
  • Uma referência de objeto é um tipo de valor.

Portanto, se você tiver uma matriz de números inteiros, a matriz é alocada no heap e os números inteiros que ele contém fazem parte do objeto da matriz no heap. Os números inteiros residem dentro do objeto de matriz na pilha, não como objetos separados, portanto, eles não são encaixotados.

Se você tem uma matriz de strings, é realmente uma matriz de referências de strings. Como as referências são tipos de valor, elas farão parte do objeto de matriz na pilha. Se você colocar um objeto de string na matriz, na verdade, coloca a referência ao objeto de string na matriz, e a string é um objeto separado no heap.

Guffa
fonte
Sim, as referências se comportam exatamente como tipos de valor, mas notei que elas geralmente não são chamadas dessa maneira ou incluídas nos tipos de valor. Veja, por exemplo (mas há muito mais como este) msdn.microsoft.com/en-us/library/s1ax56ch.aspx #
Henk Holterman
@ Henk: Sim, você está certo que as referências não estão listadas entre as variáveis ​​de tipo de valor, mas quando se trata de como a memória é alocada para elas, elas estão sob todos os aspectos, e é muito útil perceber que para entender como a alocação de memória tudo se encaixa. :)
Guffa 11/07/2009
Duvido do quinto ponto, "Uma matriz pode conter apenas tipos de valor". E quanto ao array de strings? string [] strings = nova string [4];
Sunil Purushothaman
9

Penso que no cerne da sua pergunta está um equívoco sobre os tipos de referência e valor. Provavelmente, isso é algo com o qual todos os desenvolvedores .NET e Java lutaram.

Uma matriz é apenas uma lista de valores. Se for uma matriz de um tipo de referência (digamos a string[]), a matriz é uma lista de referências a vários stringobjetos no heap, pois uma referência é o valor de um tipo de referência. Internamente, essas referências são implementadas como ponteiros para um endereço na memória. Se você deseja visualizar isso, essa matriz seria assim na memória (na pilha):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Essa é uma matriz stringque contém 4 referências a stringobjetos no heap (os números aqui são hexadecimais). Atualmente, apenas o último stringrealmente aponta para qualquer coisa (a memória é inicializada com todos os zero quando alocada), essa matriz seria basicamente o resultado desse código em C #:

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

A matriz acima estaria em um programa de 32 bits. Em um programa de 64 bits, as referências seriam duas vezes maiores ( F8AB56AAseriam 00000000F8AB56AA).

Se você possui uma matriz de tipos de valor (digamos um int[]), a matriz é uma lista de números inteiros, pois o valor de um tipo de valor é o próprio valor (daí o nome). A visualização dessa matriz seria a seguinte:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Esta é uma matriz de 4 números inteiros, em que apenas o segundo int recebe um valor (para 1174352571, que é a representação decimal desse número hexadecimal) e o restante dos números inteiros seria 0 (como eu disse, a memória é inicializada como zero e 00000000 em hexadecimal é 0 em decimal). O código que produziu essa matriz seria:

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

Essa int[]matriz também seria armazenada na pilha.

Como outro exemplo, a memória de uma short[4]matriz ficaria assim:

[ 0000, 0000, 0000, 0000 ]

Como o valor de a shorté um número de 2 bytes.

Onde um tipo de valor é armazenado, é apenas um detalhe de implementação, como Eric Lippert explica muito bem aqui , não inerente às diferenças entre os tipos de valor e referência (que é a diferença de comportamento).

Quando você passa algo para um método (seja um tipo de referência ou um tipo de valor), uma cópia do valor do tipo é realmente passada para o método. No caso de um tipo de referência, o valor é uma referência (pense nisso como um ponteiro para uma parte da memória, embora isso também seja um detalhe de implementação) e, no caso de um tipo de valor, o valor é a coisa em si.

// 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){}

O boxe ocorre apenas se você converter um tipo de valor em um tipo de referência. Este código caixas:

object o = 5;
JulianR
fonte
Eu acredito que "um detalhe de implementação" deve ter o tamanho da fonte: 50px. ;)
sisve 11/07/2009
2

Estas são ilustrações que ilustram a resposta acima de @P Daddy

insira a descrição da imagem aqui

insira a descrição da imagem aqui

E ilustrei o conteúdo correspondente no meu estilo.

insira a descrição da imagem aqui

YoungMin Park
fonte
@P Papai Eu fiz ilustrações. Por favor, verifique se há peças erradas. E eu tenho algumas perguntas adicionais. 1. Quando crio uma matriz de tipo int de 4 comprimentos, as informações de comprimento (4) também são sempre armazenadas na memória?
YoungMin Park
2. Na segunda ilustração, o endereço de matriz copiado é armazenado onde? É a mesma área de pilha na qual o endereço intArray está armazenado? É outra pilha, mas o mesmo tipo de pilha? É um tipo diferente de pilha? 3. O que significa baixo de 32 bits / alto de 32 bits? 4. O que é valor de retorno quando eu aloco o tipo de valor (neste exemplo, estrutura) na pilha usando a nova palavra-chave? É também o endereço? Quando eu estava verificando por esta declaração Console.WriteLine (valType), ele mostraria o nome completo como um objeto como ConsoleApp.ValType.
YoungMin Park
5. valType.I = 200; Esta afirmação significa que eu recebo o endereço de valType, por esse endereço eu acesso ao I e ali eu armazeno 200, mas "na pilha".
YoungMin Park
1

Uma matriz de números inteiros é alocada no heap, nada mais, nada menos. myIntegers faz referência ao início da seção em que as entradas são alocadas. Essa referência está localizada na pilha.

Se você tiver uma matriz de objetos do tipo de referência, como o tipo de objeto myObjects [], localizado na pilha, faria referência ao conjunto de valores que referenciam os objetos em si.

Para resumir, se você passar myIntegers para algumas funções, você só passará a referência para o local em que o monte real de números inteiros está alocado.

Dykam
fonte
1

Não há boxe no seu código de exemplo.

Os tipos de valor podem viver no heap, como acontece na sua matriz de ints. A matriz é alocada no heap e armazena ints, que são tipos de valor. O conteúdo da matriz é inicializado como padrão (int), que passa a ser zero.

Considere uma classe que contém um tipo de valor:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

A variável h refere-se a uma instância de HasAnInt que vive na pilha. Por acaso contém um tipo de valor. Tudo bem, 'i' simplesmente vive na pilha, pois ela está contida em uma classe. Também não há boxe neste exemplo.

Curt Nichols
fonte
1

Já foi dito por todos, mas se alguém está procurando uma amostra e documentação clara (mas não oficial) sobre heap, stack, variáveis ​​locais e variáveis ​​estáticas, consulte o artigo completo de Jon Skeet sobre Memória no .NET - o que acontece Onde

Excerto:

  1. Cada variável local (isto é, uma declarada em um método) é armazenada na pilha. Isso inclui variáveis ​​do tipo de referência - a própria variável está na pilha, mas lembre-se de que o valor de uma variável do tipo de referência é apenas uma referência (ou nula), não o próprio objeto. Os parâmetros do método também contam como variáveis ​​locais, mas se forem declarados com o modificador ref, eles não terão seu próprio slot, mas compartilharão um slot com a variável usada no código de chamada. Veja meu artigo sobre a passagem de parâmetros para mais detalhes.

  2. Variáveis ​​de instância para um tipo de referência estão sempre no heap. É aí que o próprio objeto "vive".

  3. As variáveis ​​de instância para um tipo de valor são armazenadas no mesmo contexto que a variável que declara o tipo de valor. O slot de memória para a instância contém efetivamente os slots para cada campo dentro da instância. Isso significa (dados os dois pontos anteriores) que uma variável struct declarada em um método sempre estará na pilha, enquanto uma variável struct que é um campo de instância de uma classe estará na pilha.

  4. Toda variável estática é armazenada no heap, independentemente de ser declarada em um tipo de referência ou um tipo de valor. Existe apenas um slot no total, não importa quantas instâncias sejam criadas. (Porém, não é necessário criar instâncias para que esse slot exista.) Os detalhes de exatamente em que pilha as variáveis ​​vivem são complicados, mas explicados em detalhes em um artigo do MSDN sobre o assunto.

gmaran23
fonte