CLR Internal: SyncBlock
Every Object is preceded by an ObjHeader (at a negative offset). The ObjHeader has an index to a SyncBlock. This index is 0 for the bulk of all instances, which indicates that the object shares a dummy SyncBlock with most other objects. All SyncBlocks are stored in SyncTable as an array and managed by SyncBlockCache.
The SyncBlock is primarily responsible for object synchronization. However, it is also a “kitchen sink” of sparsely allocated instance data. For instance, the default implementation of Hash() is based on the existence of a SyncTableEntry. And objects exposed to or from COM, or through context boundaries, can store sparse data here.
SyncTableEntries and SyncBlocks are allocated in non-GC memory. A weak pointer from the SyncTableEntry to the instance is used to ensure that the SyncBlock and SyncTableEntry are reclaimed (recycled) when the instance dies.
The organization of the SyncBlocks isn’t intuitive (at least to me). Here’s the explanation:
Before each Object is an ObjHeader. If the object has a SyncBlock, the ObjHeader contains a non-0 index to it.
The index is looked up in the g_pSyncTable of SyncTableEntries. This means the table is consecutive for all outstanding indices. Whenever it needs to grow, it doubles in size and copies all the original entries. The old table is kept until GC time, when it can be safely discarded.
Each SyncTableEntry has a backpointer to the object and a forward pointer to the actual SyncBlock. The SyncBlock is allocated out of a SyncBlockArray which is essentially just a block of SyncBlocks.
The SyncBlockArrays are managed by a SyncBlockCache that handles the actual allocations and frees of the blocks.
Each allocation and release has to handle free lists in the table of entries and the table of blocks.
We burn an extra 4 bytes for the pointer from the SyncTableEntry to the SyncBlock.
The reason for this is that many objects have a SyncTableEntry but no SyncBlock. That’s because someone (e.g. HashTable) called Hash() on them.
- syncblk.h

