Value-type values have to live together with the object instance in the managed heap. The thread's stack for a method only lives for the duration of a method; how can the value persist if it only exists within that stack?
A class' object size in the managed heap is the sum of its value-type fields, reference-type pointers, and additional CLR overhead variables like the Sync block index. When one assigns a value to an object's value-type field, the CLR copies the value to the space allocated within the object for that particluar field.
Take for example, a simple class with a single field.
public class EmbeddedValues
{
public int NumberField;
}
And with it, a simple testing class.
public class EmbeddedTest
{
public void TestEmbeddedValues()
{
EmbeddedValues valueContainer = new EmbeddedValues();
valueContainer.NumberField = 20;
int publicField = valueContainer.NumberField;
}
}
If you use the MSIL Disassembler provided by the .NET Framework SDK to peek at the IL code for EmbeddedTest.TestEmbeddedValues()
.method public hidebysig instance void TestEmbeddedValues() cil managed
{
// Code size 23 (0x17)
.maxstack 2
.locals init ([0] class soapextensions.EmbeddedValues valueContainer,
[1] int32 publicField)
IL_0000: nop
IL_0001: newobj instance void soapextensions.EmbeddedValues::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.s 20
IL_000a: stfld int32 soapextensions.EmbeddedValues::NumberField
IL_000f: ldloc.0
IL_0010: ldfld int32 soapextensions.EmbeddedValues::NumberField
IL_0015: stloc.1
IL_0016: ret
} // end of method EmbeddedTest::TestEmbeddedValues
Notice the CLR is being told to stfld the loaded value of "20" in the stack to the loaded EmbeddValues' NumberField field location, directly into the managed heap. Similarly, when retrieving the value, it uses ldfld instruction to directly copy the value out of that managed heap location into the thread stack. No box/unboxing happens with these types of operations.