Share via


Så här använder och felsöker du sammansättningens lossningsbarhet i .NET

.NET (Core) introducerade möjligheten att läsa in och senare ta bort en uppsättning sammansättningar. I .NET Framework användes anpassade appdomäner för detta ändamål, men .NET (Core) stöder bara en enda standardappdomän.

Lossningsbarhet stöds via AssemblyLoadContext. Du kan läsa in en uppsättning sammansättningar i en samlarbar AssemblyLoadContext, köra metoder i dem eller bara inspektera dem med reflektion och slutligen ta bort AssemblyLoadContext. Det tar bort de sammansättningar som läses in i AssemblyLoadContext.

Det finns en anmärkningsvärd skillnad mellan avlastning med hjälp av AssemblyLoadContext och användning av AppDomains. Med AppDomains tvingas avlastningen. Vid avlastning avbryts alla trådar som körs i målappdomänen, hanterade COM-objekt som skapas i målappdomänen förstörs och så vidare. Med AssemblyLoadContextär lossningen "kooperativ". AssemblyLoadContext.Unload När du anropar metoden initieras just avlastningen. Avlastningen avslutas efter:

  • Inga trådar har metoder från sammansättningarna som läses in i AssemblyLoadContext på deras anropsstackar.
  • Ingen av typerna från sammansättningarna som läses in i AssemblyLoadContext, instanser av dessa typer och själva sammansättningarna refereras av:

Använda collectible AssemblyLoadContext

Det här avsnittet innehåller en detaljerad stegvis självstudie som visar ett enkelt sätt att läsa in ett .NET-program (Core) i en samlingsbar AssemblyLoadContext, köra startpunkten och sedan ta bort den. Du hittar ett fullständigt exempel på https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.

Skapa en samlarbar sammansättningLoadContext

Härled din klass från AssemblyLoadContext och åsidosätt dess AssemblyLoadContext.Load metod. Den metoden löser referenser till alla sammansättningar som är beroenden av sammansättningar som läses in i den AssemblyLoadContext.

Följande kod är ett exempel på den enklaste anpassade AssemblyLoadContext:

class TestAssemblyLoadContext : AssemblyLoadContext
{
    public TestAssemblyLoadContext() : base(isCollectible: true)
    {
    }

    protected override Assembly? Load(AssemblyName name)
    {
        return null;
    }
}

Som du ser Load returnerar nullmetoden . Det innebär att alla beroendesammansättningar läses in i standardkontexten, och den nya kontexten innehåller endast de sammansättningar som uttryckligen läses in i den.

Om du vill läsa in vissa eller alla beroenden i AssemblyLoadContext också kan du använda AssemblyDependencyResolver i Load -metoden. AssemblyDependencyResolver Löser sammansättningsnamnen till absoluta sammansättningsfilsökvägar. Matcharen använder .deps.json-filen och sammansättningsfilerna i katalogen för huvudsammansättningen som läses in i kontexten.

using System.Reflection;
using System.Runtime.Loader;

namespace complex
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
        {
            _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
        }

        protected override Assembly? Load(AssemblyName name)
        {
            string? assemblyPath = _resolver.ResolveAssemblyToPath(name);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }
    }
}

Använda en anpassad samlingsbar sammansättningLoadContext

Det här avsnittet förutsätter att den enklare versionen av TestAssemblyLoadContext används.

Du kan skapa en instans av den anpassade AssemblyLoadContext och läsa in en sammansättning i den på följande sätt:

var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

För var och en av de sammansättningar som refereras av den inlästa sammansättningen TestAssemblyLoadContext.Load anropas metoden så att TestAssemblyLoadContext kan bestämma var sammansättningen ska hämtas från. I det här fallet returneras null den för att indikera att den ska läsas in i standardkontexten från platser som körningen använder för att läsa in sammansättningar som standard.

Nu när en sammansättning har lästs in kan du köra en metod från den. Main Kör metoden:

var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);

Main När metoden har returnerats kan du initiera avlastning genom att antingen anropa Unload metoden på den anpassade AssemblyLoadContext metoden eller ta bort referensen AssemblyLoadContextsom du har till :

alc.Unload();

Detta räcker för att lossa testsammansättningen. Därefter placerar du allt detta i en separat icke-infogad metod för att säkerställa att TestAssemblyLoadContext, Assemblyoch MethodInfo () Assembly.EntryPointinte kan hållas vid liv av stackfackreferenser (verkliga eller JIT-introducerade lokalbefolkningen). Det kan hålla liv i TestAssemblyLoadContext och förhindra lossningen.

Returnera också en svag referens till AssemblyLoadContext så att du kan använda den senare för att identifiera slutförande av avlastning.

[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
    var alc = new TestAssemblyLoadContext();
    Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

    alcWeakRef = new WeakReference(alc, trackResurrection: true);

    var args = new object[1] {new string[] {"Hello"}};
    _ = a.EntryPoint?.Invoke(null, args);

    alc.Unload();
}

Nu kan du köra den här funktionen för att läsa in, köra och ta bort sammansättningen.

WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);

Avlastningen slutförs dock inte omedelbart. Som tidigare nämnts förlitar den sig på skräpinsamlaren för att samla in alla objekt från testsammansättningen. I många fall är det inte nödvändigt att vänta tills avlastningen har slutförts. Det finns dock fall där det är användbart att veta att avlastningen har slutförts. Du kanske till exempel vill ta bort sammansättningsfilen som lästes in till den anpassade AssemblyLoadContext från disken. I sådana fall kan följande kodfragment användas. Den utlöser skräpinsamling och väntar på väntande slutförare i en loop tills den svaga referensen till den anpassade AssemblyLoadContext är inställd på , vilket anger att nullmålobjektet samlades in. I de flesta fall krävs bara en genomströmning av loopen. För mer komplexa fall där objekt som skapas av koden som körs i AssemblyLoadContext har finalizers kan det dock behövas fler pass.

for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Begränsningar

Sammansättningar som läses in i ett samlingsobjekt AssemblyLoadContext måste följa de allmänna begränsningarna för insamlingsbara sammansättningar. Följande begränsningar gäller även:

Händelsen Avlastning

I vissa fall kan det vara nödvändigt att koden läses in i en anpassad AssemblyLoadContext kod för att utföra viss rensning när avlastningen initieras. Den kan till exempel behöva stoppa trådar eller rensa starka GC-handtag. Händelsen Unloading kan användas i sådana fall. Du kan koppla en hanterare som utför den nödvändiga rensningen till den här händelsen.

Felsöka problem med avlastning

På grund av lossningens samarbetskaraktär är det lätt att glömma referenser som kan hålla grejerna i ett samlarobjekt AssemblyLoadContext vid liv och förhindra lossning. Här är en sammanfattning av entiteter (några av dem icke-lydiga) som kan innehålla referenserna:

  • Vanliga referenser som lagras utanför samlarobjektet AssemblyLoadContext som lagras i ett stackfack eller ett processorregister (metodlokalt, antingen uttryckligen skapat av användarkoden eller implicit av JIT-kompilatorn (just-in-time), en statisk variabel eller ett starkt (fästande) GC-handtag och transitivt pekar på:
    • En sammansättning som läses in i samlarobjektet AssemblyLoadContext.
    • En typ från en sådan sammansättning.
    • En instans av en typ från en sådan sammansättning.
  • Trådar som kör kod från en sammansättning som läses in i den samlarbara AssemblyLoadContext.
  • Instanser av anpassade, icke-kompriterbara AssemblyLoadContext typer som skapats i den samlarbara AssemblyLoadContext.
  • Väntande RegisteredWaitHandle instanser med motringningar inställda på metoder i den anpassade AssemblyLoadContext.

Dricks

Objektreferenser som lagras i stackfack eller processorregister och som kan förhindra avlastning av en AssemblyLoadContext kan inträffa i följande situationer:

  • När funktionsanropsresultat skickas direkt till en annan funktion, även om det inte finns någon lokal variabel som skapats av användaren.
  • När JIT-kompilatorn behåller en referens till ett objekt som var tillgängligt någon gång i en metod.

Problem med att felsöka avlastning

Det kan vara tråkigt att felsöka problem med avlastning. Du kan komma in i situationer där du inte vet vad som kan hålla en AssemblyLoadContext levande, men avlastningen misslyckas. Det bästa verktyget för att hjälpa till med det är WinDbg (eller LLDB på Unix) med SOS-plugin-programmet. Du måste hitta vad som håller en LoaderAllocator som tillhör den specifika AssemblyLoadContext levande. Med SOS-plugin-programmet kan du titta på GC-heapobjekt, deras hierarkier och rötter.

Om du vill läsa in SOS-plugin-programmet i felsökningsprogrammet anger du något av följande kommandon på kommandoraden för felsökningsprogrammet.

I WinDbg (om den inte redan har lästs in):

.loadby sos coreclr

I LLDB:

plugin load /path/to/libsosplugin.so

Nu ska du felsöka ett exempelprogram som har problem med avlastning. Källkoden är tillgänglig i avsnittet Exempel på källkod . När du kör den under WinDbg bryter programmet sig in i felsökningsprogrammet direkt efter att du försökt söka efter att avlastningen lyckades. Du kan sedan börja leta efter de skyldiga.

Dricks

Om du felsöker med LLDB på Unix har ! SOS-kommandona i följande exempel inte framför sig.

!dumpheap -type LoaderAllocator

Det här kommandot dumpar alla objekt med ett typnamn som innehåller LoaderAllocator i GC-heapen. Här är ett exempel:

         Address               MT     Size
000002b78000ce40 00007ffadc93a288       48
000002b78000ceb0 00007ffadc93a218       24

Statistics:
              MT    Count    TotalSize Class Name
00007ffadc93a218        1           24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288        1           48 System.Reflection.LoaderAllocator
Total 2 objects

I delen "Statistik:" kontrollerar du (MTMethodTable) som tillhör System.Reflection.LoaderAllocator, vilket är det objekt som du bryr dig om. I listan i början letar du sedan upp posten med MT den som matchar den och hämtar själva objektets adress. I det här fallet är det "000002b78000ce40".

Nu när du känner till objektets LoaderAllocator adress kan du använda ett annat kommando för att hitta dess GC-rötter:

!gcroot 0x000002b78000ce40

Det här kommandot dumpar kedjan med objektreferenser som leder till instansen LoaderAllocator . Listan börjar med roten, som är entiteten som håller liv i LoaderAllocator och därmed är kärnan i problemet. Roten kan vara en stackplats, ett processorregister, ett GC-handtag eller en statisk variabel.

Här är ett exempel på utdata från gcroot kommandot:

Thread 4ac:
    000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
        rbp-20: 000000cf9499dd90
            ->  000002b78000d328 System.Reflection.RuntimeMethodInfo
            ->  000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
            ->  000002b78000d1d0 System.RuntimeType
            ->  000002b78000ce40 System.Reflection.LoaderAllocator

HandleTable:
    000002b7f8a81198 (strong handle)
    -> 000002b78000d948 test.Test
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

    000002b7f8a815f8 (pinned handle)
    -> 000002b790001038 System.Object[]
    -> 000002b78000d390 example.TestInfo
    -> 000002b78000d328 System.Reflection.RuntimeMethodInfo
    -> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
    -> 000002b78000d1d0 System.RuntimeType
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

Found 3 roots.

Nästa steg är att ta reda på var roten finns så att du kan åtgärda den. Det enklaste fallet är när roten är en stackplats eller ett processorregister. I så fall gcroot visar namnet på den funktion vars ram innehåller roten och tråden som kör den funktionen. Det svåra fallet är när roten är en statisk variabel eller ett GC-handtag.

I föregående exempel är den första roten en lokal typ System.Reflection.RuntimeMethodInfo som lagras i ramen för funktionen example.Program.Main(System.String[]) på adressen rbp-20 (rbp är processorregistret rbp och -20 är en hexadecimal förskjutning från det registret).

Den andra roten är en normal (stark) GCHandle som innehåller en referens till en instans av test.Test klassen.

Den tredje roten är en fäst GCHandle. Den här är faktiskt en statisk variabel, men tyvärr finns det inget sätt att säga. Statiska värden för referenstyper lagras i en hanterad objektmatris i interna körningsstrukturer.

Ett annat fall som kan förhindra avlastning av en AssemblyLoadContext är när en tråd har en ram av en metod från en sammansättning som läses in i stacken AssemblyLoadContext . Du kan kontrollera detta genom att dumpa hanterade anropsstackar för alla trådar:

~*e !clrstack

Kommandot betyder "apply to all threads the !clrstack command". Följande är utdata från kommandot för exemplet. Tyvärr har INTE LLDB på Unix något sätt att tillämpa ett kommando på alla trådar, så du måste växla trådar manuellt och upprepa clrstack kommandot. Ignorera alla trådar där felsökaren säger "Det går inte att gå den hanterade stacken".

OS Thread Id: 0x6ba8 (0)
        Child SP               IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
        Child SP               IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
        Child SP               IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
        Child SP               IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]

Som du ser har test.Program.ThreadProc()den sista tråden . Det här är en funktion från sammansättningen som läses in i AssemblyLoadContext, så att den AssemblyLoadContext håller liv i.

Exempel på källkod

Följande kod som innehåller problem med avlastning används i föregående felsökningsexempel.

Huvudtestningsprogram

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;

namespace example
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        public TestAssemblyLoadContext() : base(true)
        {
        }
        protected override Assembly? Load(AssemblyName name)
        {
            return null;
        }
    }

    class TestInfo
    {
        public TestInfo(MethodInfo? mi)
        {
            _entryPoint = mi;
        }

        MethodInfo? _entryPoint;
    }

    class Program
    {
        static TestInfo? entryPoint;

        [MethodImpl(MethodImplOptions.NoInlining)]
        static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
        {
            var alc = new TestAssemblyLoadContext();
            testAlcWeakRef = new WeakReference(alc);

            Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
            if (a == null)
            {
                testEntryPoint = null;
                Console.WriteLine("Loading the test assembly failed");
                return -1;
            }

            var args = new object[1] {new string[] {"Hello"}};

            // Issue preventing unloading #1 - we keep MethodInfo of a method
            // for an assembly loaded into the TestAssemblyLoadContext in a static variable.
            entryPoint = new TestInfo(a.EntryPoint);
            testEntryPoint = a.EntryPoint;

            var oResult = a.EntryPoint?.Invoke(null, args);
            alc.Unload();
            return (oResult is int result) ? result : -1;
        }

        static void Main(string[] args)
        {
            WeakReference testAlcWeakRef;
            // Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
            MethodInfo? testEntryPoint;
            int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);

            for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            System.Diagnostics.Debugger.Break();

            Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
        }
    }
}

Programmet läses in i TestAssemblyLoadContext

Följande kod representerar den test.dll skickas till ExecuteAndUnload metoden i huvudtestningsprogrammet.

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace test
{
    class Test
    {
    }

    class Program
    {
        public static void ThreadProc()
        {
            // Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
            Thread.Sleep(Timeout.Infinite);
        }

        static GCHandle handle;
        static int Main(string[] args)
        {
            // Issue preventing unloading #3 - normal GC handle
            handle = GCHandle.Alloc(new Test());
            Thread t = new Thread(new ThreadStart(ThreadProc));
            t.IsBackground = true;
            t.Start();
            Console.WriteLine($"Hello from the test: args[0] = {args[0]}");

            return 1;
        }
    }
}