.NET Framework FAQ : How are Generics implemented?
Generics have native support in IL and the CLR itself. When you compile
generic server-side code, the compiler compiles it into IL, just like any other
type. However, the IL only contains parameters or place holders for the actual
specific types. In addition, the metadata of the generic server contains generic
information such as constraints.
The client-side compiler uses that generic metadata to support type safety.
When the client provides a type arguments, the client's compiler substitutes the
generic type parameter in the server metadata with the specified type. This
provides the client's compiler with type-specific definition of the server, as
if generics were never involved. At run time, the actual machine code produced
depends on whether the specified types are value or reference type. If the
client specifies a value type, the JIT compiler replaces the generic type
parameters in the IL with the specific value type, and compiles it to native
code. However, the JIT compiler keeps track of type-specific server code it
already generated. If the JIT compiler is asked to compile the generic server
with a value type it has already compiled to machine code, it simply returns a
reference to that server code. Because the JIT compiler uses the same
value-type-specific server code in all further encounters, there is no code
bloating.
If the client specifies a reference type, then the JIT compiler replaces the
generic parameters in the server IL with object, and compiles it into native
code. That code will be used in any further requests for a reference type
instead of a generic type parameter. Note that this way the JIT compiler only
reuses actual code. Instances are still allocated according to their size off
the managed heap, and there is no casting.