Several of my colleagues and I recently began looking into how assemblies were loaded in the .NET CLR for a web application that we're working on. The application runs several instances within each of several application pools that all used the same set of large assemblies. What we found was, that since we'd paid little attention to how assemblies were loaded to that point, we had assemblies being loaded multiple times contributing to a huge memory footprint for the application.
To fully understand the problem and the partial solution we found, I'll review how an application in IIS and the .NET CLR is logically structured.
|
| Logical Runtime Structure of IIS Hosted Websites |
Global Assembly Cache (GAC)
The GAC is a cache of Common Language Infrastructure assemblies available to be shared by multiple applications. Assemblies loaded from the GAC are loaded with Full Trust. Note that in .NET 4, Code Access Security (CAS) has changed significantly and permissions are determined largely by the permissions of the executing account.
Application Pools
Application Pools (App Pools) are grouped sets of Web Applications under IIS that share the same W3WP worker process. Each application in an app pool runs under the same service account. Often sys admins create a separate application pool for each web application to create isolation. Generally, you'll have a single worker process for an application, though you can configure multiple (referred to as a Web Garden). Note: Generally you want to avoid Web Gardens as they can create all sorts of problems.
Thread
Within a worker process, one or more threads are available. Threads are like workers on an assembly line with App Domains being the different stations. A thread does all the work inside of an application and can only work on one App Domain at any given time, though they can bounce back and forth over the lifecycle of the application.
AppDomain
An application domain is a unit of isolation within the .NET framework. By default, every application has 1 AppDomain. Within an AppDomain, there are three contexts for assemblies:
- default-load context: Resolved from the GAC or private application bin.
- load-from: Loaded using the Assembly.LoadFrom method.
- reflection-only: Loaded only for reflection purposes.
The CLR maintains a SharedDomain at the worker process level for assemblies it determines to be "domain-neutral." You can increase the chances of domain-neutral loading by placing assemblies in the GAC or using the LoaderOptimizationAttribute on the main method.
Now with all that background information out of the way, we found that placing those large assemblies in the GAC caused the CLR to load them as domain-neutral, sharing them in memory across applications in the same application pool. This significantly reduced our memory footprint and impacted resource demands on our servers.
The moral of the story is that we often don't pay enough attention to how our applications are deployed. While I'm not recommending the GAC for every solution, evaluating how your application uses resources can lead to a massive performance impact for your end users.
Reference Links: