5

[.NET Internals 08] What about Large Object Heap (LOH)?

 2 years ago
source link: https://www.codejourney.net/2018/09/net-internals-08-what-about-large-object-heap-loh/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

So far within the .NET Internals series we focused on Small Object Heap (SOH). We know, for instance, that the LOH is not compacted (by default) during garbage collection. So how is it actually handled by the GC?

(De)allocating objects on LOH

As we know from the second post, during allocating the memory only objects of size greater than 85,000 bytes are placed on LOH. There are also some exceptions, like arrays of double which are put on LOH (in 32-bit architectures only) as soon as they reach 1000 (or more) elements (not something around 10626 elements as could be expected). This is quite important to know to be aware what kind of objects have impact on heap fragmentation (more details below).

So we know when the objects are allocated on LOH, but when are they deallocated?

LOH is collected in the same time as the generation 2 collection occurs. It can be triggered if memory threshold for either gen 2 or LOH is exceeded. Conditions for garbage collection can be found in this post.

That’s why keeping large LOH may affect the GC’s – and the whole application’s – performance.

Garbage collection on LOH

LOH fragmentation

The reason why Large Object Heap is not compacted (by default) is because it’s used to store big objects (>85,000 bytes). Copying such amounts of data would seriously incur the performance of garbage collection process.

Anyway, memory of objects allocated on the LOH is reclaimed so it may eventually become fragmented:

LOH fragmentation

We’ll see below how, but .NET keeps track of “Free space” memory blocks to know which chunks are available for new allocations on LOH. When allocating it looks for a block large enough to store the whole object.

However, imagine that there are 2 free space blocks next to each other. Both were marked free, but they represented different objects (maybe they were next to each other on the heap because one referenced the other). What do you think GC will do? Will it treat them as two separate free memory blocks, making less chances for the next allocated object to fit into one of them?

Fortunately not. GC has an optimization introduced which makes such adjacent free memory chunks “merged” together:

Free memory chunks “merged” on LOH

How does GC do it? Let’s see in more details.

Free memory representation on LOH

Instead of compacting Large Object Heap, garbage collector keeps the address ranges of not used large objects in a Free Space Table:

Free Space Table, source

As you can see on the figure above, as soon as gen 2 collection run, address ranges of two unused objects were just added to the Free Space Table.

Now you can see that “merging” two adjacent free memory chunks is just a simple addition operation (or modification of one number in the table).

Allocating memory on LOH

As soon as a new large (>85,000 bytes or applicable array) object is to be allocated on the managed heap, GC looks for a single “Free Space” block to hold it. However, it’s rather unlikely that the particular object’s size will fit into one of the free memory chunks. In that case, a new object will be allocated on the top of the heap (just after ‘Object D’ on the figure above).

It may happen that the memory obtained from the operating system for LOH is already used (read here for more info about memory). Garbage collector then asks the operating system for more memory segments to be acquired for LOH. If it fails, gen 2 collection is triggered hoping that some memory blocks will be freed and then the allocation will be possible.

Let’s now think about it for a while. We said previously that LOH collection triggers gen 2 collection. So trying to clean-up the Large Object Heap every time an allocation on it is made would be a potential performance killer.

How does GC solve this issue? Well, in fact, after a lot of optimizations introduced to LOH management in .NET 4.5, the GC takes the following actions in order to make a new allocation of a large object:

  • firstly, the GC tries to allocate new objects into one of the free space “holes” on the LOH (it’s quite simple to calculate whether any chunk is large enough to store the object knowing ranges of free space blocks from the Free Space Table).
  • if the above fails – garbage collector prefers to allocate new large objects at the end of the heap. Even though it may involve asking the OS for more memory segments, it’s been found to be easier and less consuming operation than performing full GC hoping to free some memory chunks on the LOH first.
  • only if the above fails (LOH cannot be “extended”) – GC triggers gen 2 collection hoping to free some additional space that could be used for a new allocation.

What’s worth noticing is that this actions order seems to be good for performance, but can sometimes be a reason of memory fragmentation.

Manual LOH compaction

As you should already know from the previous article, LOH can be compacted programmatically, by setting the GCSettings.LargeObjectHeapCompactionMode property. The simplest way to force LOH compaction is as the following snippet presents:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect();

There might be some niche cases in which LOH compaction may be useful. More details and discussions can be found for example here.

Best practices working with LOH objects

We can try to simply design our applications to use the less possible number of large objects, but let’s not exaggerate. We are living in a world where 8GBs of 2133MHz RAM costs sometimes less than 100$, so the memory is generally cheap 😉

In principle, the rule seems to be simple: large objects we allocate should be reused (e.g. cached) as much as possible.

We should just keep in mind that allocation of large objects can be costly, because of a need to perform gen 2 collection in some cases before the object is allocated.

An example of potentially problematic large object can be a ViewState used in ASP.NET applications, size of which can easily exceed 85K. There are some good articles explaining how to not stupidly incur ASP.NET app’s performance using it, for instance this one.

There are also a lot of tools which can be used to measure the memory state and performance of our applications (also its internal mechanisms like garbage collection, heaps compaction etc.) which we’ll surely cover in one of the next posts within the series 🙂

Summary

Today we examined – previously a bit forgotten – Large Object Heap. We saw how the information about free memory blocks on it is stored by .NET Framework and how new objects are allocated on it.

I think it’s one of the next internal concepts of .NET worth knowing and understanding, even though in the common scenarios and business applications you probably won’t get into troubles with LOH. However, it may be practical and useful to know when working with some more memory-demanding applications like games.

I hope this post clarifies some LOH topics for you.

Let me know if there are any topics you’d be interested in reading about. I’m here to provide some value to you, so I’m open for your criticism and suggestions 🙂

Stay tuned!

Don't miss any new content!

Sign up for my newsletter 🙂

I agree to receive new updates notification emails from codejourney.net

Leave this field empty if you're human:

Related Posts


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK