Lessons Learned: .NET Framework Assembly Loading, Memory Optimization

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:

Author’s Note: This article reflects my personal professional experience and opinions. While my insights are informed by my professional history, these views are my own and do not represent the official position of my former employer.

About the Author: Jacob Marks is an engineering leader with over 20 years of experience, including a decade at Amazon Web Services (AWS) where he led teams in EC2 Core Platform and the development of the AWS Payment Cryptography service.

Labels

.NET .NET 10 .NET 3.5 Active Directory AD DS Adoption AI AI coding AI Ethics AI Hype Alerts Amazon Cognito Amazon DLM Amazon Q Anthropic AppDomain Architecture Artificial Intelligence Asia Pacific Sydney ASP.net ASPxGridView Audit Readiness Auto Recovery Automation AWS AWS Certified AWS Lambda AWS Payment Cryptography AWS SDK AWS Security Specialty Azure Azure DevOps Server Backup BIG-IP C# Career Growth Cartes Bancaires CB Certificate Bundle Certification Claude Cloud Cloud Certification Cloud Hosting Cloud Security CloudWatch CLR Content Query Cost Optimization Credentials CyberChef Database Defense Industry Deloitte Developer Tools Developers DevEx DevExpress DevOps DISA Disk Space DISM Distributed Systems DoD DoD CC SRG DUKPT EBS EC2 Engineering Engineering Leadership Engineering Management EnPasFltV2 Enterprise Event Receiver Exam F5 Federal IT FedRAMP Fintech FISMA GAC Generative AI GitHub gMSA GovCloud Government Compliance GridView Hardware Security Modules HSM IAM Identity Management IIS Infra Infrastructure as Code IT Tools Jacob Marks JavaScript jQuery Lambda Leadership Linqpad LLM lsass.exe LTM Memory Optimization Mentorship Microsoft Migration Multi-Region Keys NACL Native AOT Network Architecture Networking NIST ODBC Open Source Payment Cryptography Payments PCI Compliance Performance Platform Platform Architecture Power Tools PowerShell Python re:Invent Reachability Analyzer Redshift Relationships List Replace Root Volume SAA-C00 SAP-C00 Security Security Group Serverless SES SharePoint SharePoint 2010 Site Reliability SMTP Snapshot Software Engineering Solutions Architect Solutions Architect Professional SP 2007 SPAWAR SSL STIG Storage Strategy Sydney SysAdmin Team Foundation Server Team Utilities Tech Industry Technical Depth Technology TFS Tools Troubleshooting Upgrade Visual Studio VPC VPC Flow Logs Web Development WebPart WinDirStat Windows Server Windows Server 2025 WinForms