Skip to content

Commit 6fecc25

Browse files
authored
[mono] Initial support for unloadable ALCs (#77399)
* [mono] Add LoaderAllocator type, whose instances are used to detect whenever a collectible ALC has managed references. * [mono] Add an implicit GC reference between objects allocated from a collectible ALC and its LoaderAllocator object. * [mono] Add a new hash table which is similar to MonoGHashTable, but it doesn't keep the key/value objects alive by itself. * [mono] Add a keepalive field to some reflection objects to keep the alc alive if user code holds a reference to them. Change the reflection hashes in MonoMemoryManager to weak hashes so the hashes themselves don't keep the alc alive. * Fix reflection hashes. * [mono] Optimize the case when mono_method_get_signature_checked () is called with a non-zero context and a non-generic signature. * Free memory manager caches. * [mono] Store static variables with GC references in collectible alcs on the GC heap. Normally, static variables are stored in an array inside MonoVTable which is registered as a GC root. For collectible alcs, this would not work, since GC references in these arrays would keep the alc alive. Instead, store them in arrays referenced by the LoaderAllocator object. This assumes the static variables will no longer be accessed after the LoaderAllocator object dies. * Add basic unload functionality. * Fix weak hashes. * Free MonoJitInfos belonging to unloaded memory managers. * Avoid returning collectible types from mono_metadata_get_shared_type (). * Add docs. * Fix the build. * Fix the build. * Disable unloading for now.
1 parent d70d2a9 commit 6fecc25

34 files changed

+1206
-107
lines changed

docs/design/mono/unloadability.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
Design
3+
------
4+
5+
The design is based on docs/design/features/unloadability.md.
6+
7+
Memory managers
8+
---------------
9+
10+
Most runtime memory is owned by MonoMemoryManager structures, which are owned by one or more ALCs. For example,
11+
if type T1 is from ALC1, and T2 is from ALC2, then the memory related to the generic instance T1<T2> will
12+
be owned by the memory manager for [ALC1, ALC2]. A memory manager is collectible if one of its ALCs is collectible.
13+
14+
LoaderAllocator objects
15+
-----------------------
16+
17+
Each memory manager has a corresponding LoaderAllocator object. This object is referenced explicitly or
18+
implicitly by every managed object which is related to the memory manager. Until unloading starts,
19+
the memory manager keeps a strong reference to this object.
20+
21+
For objects whose type is from a collectible ALC, the GC maintains an implicit reference by marking
22+
the LoaderAllocator object when the object is marked.
23+
24+
Reflection objects referencing the ALC have an explicit 'keepalive' field which points to the
25+
corresponding LoaderAllocator object.
26+
27+
When the LoaderAllocator object is collected by the GC, it means there are no more managed
28+
references to its memory allocator, so it can be unloaded.
29+
30+
Reflection caches and static variables
31+
--------------------------------------
32+
33+
Reflection objects are kept in caches inside the runtime. These caches keep strong references to the
34+
reflection objects. This doesn't work for collectible ALCs, since the objects keep the LoaderAllocator
35+
object alive. So for collectible ALCs, we use a different kind of hash table whose keys/values are
36+
stored in object[] arrays inside the LoaderAllocator object, and the hash table holds a weak reference
37+
to these arrays. This means that the reflection objects are only kept alive by the LoaderAllocator and
38+
by application code.
39+
40+
Similarly, normal static variables are treated as GC roots so any static variable pointing to an object
41+
inside the ALC would keep the ALC alive. Instead, we store static variables inside arrays in the
42+
LoaderAllocator object. Normal reference type variable are allocated an entry in a pinned object[],
43+
and their address becomes an interior pointer to the pinned array element. For valuetypes, we
44+
allocate a pinned boxed object and the static variable address becomes the address of the unboxed
45+
object.
46+
47+
Unloading process
48+
-----------------
49+
50+
- The app calls AssemblyLoadContext.Unload ()
51+
- The runtime iterates over the memory managers of the ALC. Each ALC holds a strong reference to its
52+
LoaderAllocator object. We change the strong reference to a weak one, allowing the LoaderAllocator
53+
to the collected.
54+
- The app eventually drops all implicit and explicit references to the LoaderAllocator object.
55+
- The finalizer of the LoaderAllocator object calls into the runtime to free the corresponding
56+
memory manager.
57+
- When all memory managers referencing the ALC are freed, the ALC itself is freed.
58+
59+
See the document in the Design section about the LoaderAllocatorScout object.
60+
61+
Remaining work
62+
---------------
63+
64+
- Managed frames
65+
Frames belonging to code in the ALC should keep the ALC alive. This can be implemented by
66+
having all methods allocate a volatile local variable and store a reference to their LoaderAllocator object
67+
into it.
68+
- Thread abort
69+
Although its not part of current .NET APIs, it might be useful to have a way to abort threads executing code
70+
in an ALC, similarly to how domain unloads worked previously.
71+
- Reflection pointers
72+
Icalls which take a assembly/type etc. handle as parameter need to keep the ALC alive, otherwise there will
73+
be subtle races.
74+
- Boehm GC support
75+
- TLS variables
76+
- Enable/disable compiler flag
77+
- Profiling, perf counters, etc. support
78+
- Diagnostics support, i.e. what keeps an ALC alive
79+
- Testing
80+
- Leak detection

src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
<Compile Include="$(BclSourcesRoot)\System\Reflection\ConstructorInvoker.Mono.cs" />
209209
<Compile Include="$(BclSourcesRoot)\System\Reflection\CustomAttribute.cs" />
210210
<Compile Include="$(BclSourcesRoot)\System\Reflection\FieldInfo.Mono.cs" />
211+
<Compile Include="$(BclSourcesRoot)\System\Reflection\LoaderAllocator.cs" />
211212
<Compile Include="$(BclSourcesRoot)\System\Reflection\MemberInfo.Mono.cs" />
212213
<Compile Include="$(BclSourcesRoot)\System\Reflection\MethodBase.Mono.cs" />
213214
<Compile Include="$(BclSourcesRoot)\System\Reflection\MethodInvoker.Mono.cs" />

src/mono/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.Mono.cs

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ public sealed partial class AssemblyBuilder : Assembly
179179
//
180180
#region Sync with RuntimeAssembly.cs and ReflectionAssembly in object-internals.h
181181
internal IntPtr _mono_assembly;
182+
private LoaderAllocator? m_keepalive;
182183

183184
private UIntPtr dynamic_assembly; /* GC-tracked */
184185
private ModuleBuilder[] modules;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.InteropServices;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace System.Reflection
8+
{
9+
// See src/coreclr/System.Private.CoreLib/src/System/Reflection/LoaderAllocator.cs for
10+
// more comments
11+
internal sealed class LoaderAllocatorScout
12+
{
13+
internal IntPtr m_native;
14+
15+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
16+
private static extern bool Destroy(IntPtr native);
17+
18+
internal LoaderAllocatorScout(IntPtr native)
19+
{
20+
m_native = native;
21+
}
22+
23+
~LoaderAllocatorScout()
24+
{
25+
if (!Destroy(m_native))
26+
{
27+
GC.ReRegisterForFinalize(this);
28+
}
29+
}
30+
}
31+
32+
//
33+
// This object is allocated by the runtime, every object
34+
// in a collectible alc has an implicit reference to it, maintained by
35+
// the GC, or an explicit reference through a field.
36+
//
37+
[StructLayout(LayoutKind.Sequential)]
38+
internal sealed class LoaderAllocator
39+
{
40+
#region Sync with MonoManagedLoaderAllocator in object-internals.h
41+
#pragma warning disable CA1823, 414, 169
42+
private LoaderAllocatorScout m_scout;
43+
// These point to objects created by the runtime which are kept
44+
// alive by this LoaderAllocator
45+
private object[]? m_slots;
46+
private object[]? m_hashes;
47+
private int m_nslots;
48+
#pragma warning restore CA1823, 414, 169
49+
#endregion
50+
51+
private LoaderAllocator(IntPtr native)
52+
{
53+
m_scout = new LoaderAllocatorScout(native);
54+
}
55+
}
56+
}

src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs

+2
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ public unsafe UnmanagedMemoryStreamForModule(byte* pointer, long length, Module
6464

6565
//
6666
// KEEP IN SYNC WITH _MonoReflectionAssembly in /mono/mono/metadata/object-internals.h
67+
// and AssemblyBuilder.cs.
6768
//
6869
#region VM dependency
6970
private IntPtr _mono_assembly;
71+
private LoaderAllocator? m_keepalive;
7072
#endregion
7173

7274
internal IntPtr GetUnderlyingNativeHandle() { return _mono_assembly; }

src/mono/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.Mono.cs

+8
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,20 @@ internal IntPtr NativeALC
2424
[DynamicDependency(nameof(_nativeAssemblyLoadContext))]
2525
private IntPtr InitializeAssemblyLoadContext(IntPtr thisHandlePtr, bool representsTPALoadContext, bool isCollectible)
2626
{
27+
if (isCollectible)
28+
KeepLoaderAllocator();
2729
using (SafeStringMarshal handle = RuntimeMarshal.MarshalString(Name))
2830
{
2931
return InternalInitializeNativeALC(thisHandlePtr, handle.Value, representsTPALoadContext, isCollectible);
3032
}
3133
}
3234

35+
// Keep the type alive since instances are created by the runtime
36+
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(LoaderAllocator))]
37+
private static void KeepLoaderAllocator()
38+
{
39+
}
40+
3341
[MethodImplAttribute (MethodImplOptions.InternalCall)]
3442
private static extern void PrepareForAssemblyLoadContextRelease (IntPtr nativeAssemblyLoadContext, IntPtr assemblyLoadContextStrong);
3543

src/mono/System.Private.CoreLib/src/System/Type.Mono.cs

+3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
using System.Reflection;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
78
using System.Runtime.Versioning;
89
using System.Threading;
910

1011
namespace System
1112
{
13+
[StructLayout(LayoutKind.Sequential)]
1214
public partial class Type
1315
{
1416
#region keep in sync with object-internals.h
1517
internal RuntimeTypeHandle _impl;
18+
internal LoaderAllocator? m_keepalive;
1619
#endregion
1720

1821
internal IntPtr GetUnderlyingNativeHandle()

src/mono/mono/metadata/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ set(metadata_common_sources
107107
mono-hash.h
108108
mono-conc-hash.c
109109
mono-conc-hash.h
110+
weak-hash.c
111+
weak-hash.h
110112
mono-ptr-array.h
111113
monitor.h
112114
object.c

src/mono/mono/metadata/assembly-load-context.c

+75-8
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,26 @@ static void
3838
mono_alc_init (MonoAssemblyLoadContext *alc, gboolean collectible)
3939
{
4040
MonoLoadedImages *li = g_new0 (MonoLoadedImages, 1);
41+
42+
// FIXME:
43+
collectible = FALSE;
44+
4145
mono_loaded_images_init (li, alc);
4246
alc->loaded_images = li;
4347
alc->loaded_assemblies = NULL;
44-
alc->memory_manager = mono_mem_manager_new (&alc, 1, collectible);
4548
alc->generic_memory_managers = g_ptr_array_new ();
4649
mono_coop_mutex_init (&alc->memory_managers_lock);
4750
alc->unloading = FALSE;
4851
alc->collectible = collectible;
4952
alc->pinvoke_scopes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
5053
mono_coop_mutex_init (&alc->assemblies_lock);
5154
mono_coop_mutex_init (&alc->pinvoke_lock);
55+
56+
alc->memory_manager = mono_mem_manager_new (&alc, 1, collectible);
57+
58+
if (collectible)
59+
/* Eagerly create the loader alloc object for the main memory manager */
60+
mono_mem_manager_get_loader_alloc (alc->memory_manager);
5261
}
5362

5463
static MonoAssemblyLoadContext *
@@ -98,6 +107,7 @@ mono_alc_cleanup_assemblies (MonoAssemblyLoadContext *alc)
98107
// The minimum refcount on assemblies is 2: one for the domain and one for the ALC.
99108
// The domain refcount might be less than optimal on netcore, but its removal is too likely to cause issues for now.
100109
GSList *tmp;
110+
const char *name = alc->name ? alc->name : "<default>";
101111

102112
// Remove the assemblies from loaded_assemblies
103113
for (tmp = alc->loaded_assemblies; tmp; tmp = tmp->next) {
@@ -107,8 +117,8 @@ mono_alc_cleanup_assemblies (MonoAssemblyLoadContext *alc)
107117
loaded_assemblies = g_slist_remove (loaded_assemblies, assembly);
108118
alcs_unlock ();
109119

110-
mono_assembly_decref (assembly);
111-
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC [%p], removing assembly %s[%p] from loaded_assemblies, ref_count=%d\n", alc, assembly->aname.name, assembly, assembly->ref_count);
120+
//mono_assembly_decref (assembly);
121+
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC '%s'[%p], removing assembly %s[%p] from loaded_assemblies, ref_count=%d\n", name, alc, assembly->aname.name, assembly, assembly->ref_count);
112122
}
113123

114124
// Release the GC roots
@@ -122,7 +132,7 @@ mono_alc_cleanup_assemblies (MonoAssemblyLoadContext *alc)
122132
MonoAssembly *assembly = (MonoAssembly *)tmp->data;
123133
if (!assembly->image || !image_is_dynamic (assembly->image))
124134
continue;
125-
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC [%p], dynamic assembly %s[%p], ref_count=%d", alc, assembly->aname.name, assembly, assembly->ref_count);
135+
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC '%s'[%p], dynamic assembly %s[%p], ref_count=%d", name, alc, assembly->aname.name, assembly, assembly->ref_count);
126136
if (!mono_assembly_close_except_image_pools (assembly))
127137
tmp->data = NULL;
128138
}
@@ -134,7 +144,7 @@ mono_alc_cleanup_assemblies (MonoAssemblyLoadContext *alc)
134144
continue;
135145
if (!assembly->image || image_is_dynamic (assembly->image))
136146
continue;
137-
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC [%p], non-dynamic assembly %s[%p], ref_count=%d", alc, assembly->aname.name, assembly, assembly->ref_count);
147+
mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_ASSEMBLY, "Unloading ALC '%s'[%p], non-dynamic assembly %s[%p], ref_count=%d", name, alc, assembly->aname.name, assembly, assembly->ref_count);
138148
if (!mono_assembly_close_except_image_pools (assembly))
139149
tmp->data = NULL;
140150
}
@@ -173,15 +183,21 @@ mono_alc_cleanup (MonoAssemblyLoadContext *alc)
173183

174184
mono_alc_cleanup_assemblies (alc);
175185

176-
mono_mem_manager_free (alc->memory_manager, FALSE);
186+
// FIXME: Do it for every memory manager
187+
mono_gchandle_free_internal (alc->memory_manager->loader_allocator_handle);
188+
alc->memory_manager->loader_allocator_handle = NULL;
189+
190+
// FIXME: Change to FALSE
191+
mono_mem_manager_free (alc->memory_manager, TRUE);
177192
alc->memory_manager = NULL;
178193

179194
/*for (int i = 0; i < alc->generic_memory_managers->len; i++) {
180195
MonoGenericMemoryManager *memory_manager = (MonoGenericMemoryManager *)alc->generic_memory_managers->pdata [i];
181196
mono_mem_manager_free_generic (memory_manager, FALSE);
182197
}*/
183-
g_ptr_array_free (alc->generic_memory_managers, TRUE);
184-
mono_coop_mutex_destroy (&alc->memory_managers_lock);
198+
// FIXME:
199+
//g_ptr_array_free (alc->generic_memory_managers, TRUE);
200+
//mono_coop_mutex_destroy (&alc->memory_managers_lock);
185201

186202
mono_gchandle_free_internal (alc->gchandle);
187203
alc->gchandle = NULL;
@@ -260,6 +276,10 @@ ves_icall_System_Runtime_Loader_AssemblyLoadContext_PrepareForAssemblyLoadContex
260276
MonoGCHandle strong_gchandle = (MonoGCHandle)strong_gchandle_ptr;
261277
MonoAssemblyLoadContext *alc = (MonoAssemblyLoadContext *)alc_pointer;
262278

279+
// FIXME:
280+
if (!alc->collectible)
281+
return;
282+
263283
g_assert (alc->collectible);
264284
g_assert (!alc->unloading);
265285
g_assert (alc->gchandle);
@@ -270,6 +290,12 @@ ves_icall_System_Runtime_Loader_AssemblyLoadContext_PrepareForAssemblyLoadContex
270290
MonoGCHandle weak_gchandle = alc->gchandle;
271291
alc->gchandle = strong_gchandle;
272292
mono_gchandle_free_internal (weak_gchandle);
293+
294+
mono_mem_manager_start_unload (alc->memory_manager);
295+
mono_alc_memory_managers_lock (alc);
296+
for (guint i = 0; i < alc->generic_memory_managers->len; i++)
297+
mono_mem_manager_start_unload ((MonoMemoryManager *)g_ptr_array_index (alc->generic_memory_managers, i));
298+
mono_alc_memory_managers_unlock (alc);
273299
}
274300

275301
gpointer
@@ -655,3 +681,44 @@ mono_alc_get_all_loaded_assemblies (void)
655681
alcs_unlock ();
656682
return assemblies;
657683
}
684+
685+
686+
MonoBoolean
687+
ves_icall_System_Reflection_LoaderAllocatorScout_Destroy (gpointer native)
688+
{
689+
/* Not enabled yet */
690+
return FALSE;
691+
692+
#if 0
693+
MonoMemoryManager *mem_manager = (MonoMemoryManager *)native;
694+
MonoAssemblyLoadContext *alc;
695+
696+
MonoGCHandle loader_handle = mem_manager->loader_allocator_weak_handle;
697+
if (mono_gchandle_get_target_internal (loader_handle))
698+
return FALSE;
699+
700+
/*
701+
* The weak handle is NULL, meaning the managed LoaderAllocator object is dead, we can
702+
* free the native side.
703+
*/
704+
for (int i = 0; i < mem_manager->n_alcs; ++i) {
705+
alc = mem_manager->alcs [i];
706+
mono_alc_memory_managers_lock (alc);
707+
g_ptr_array_remove (alc->generic_memory_managers, mem_manager);
708+
mono_alc_memory_managers_unlock (alc);
709+
}
710+
711+
alc = mem_manager->alcs [0];
712+
713+
// FIXME: Generic memory managers might need to be destroyed in a specific order/together,
714+
// hold ref counts on alcs etc.
715+
if (mem_manager == alc->memory_manager) {
716+
mono_alc_cleanup (alc);
717+
} else {
718+
// FIXME: Use debug_unload=FALSE
719+
mono_mem_manager_free (mem_manager, TRUE);
720+
}
721+
722+
return TRUE;
723+
#endif
724+
}

src/mono/mono/metadata/class-internals.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ struct _MonoClassField {
155155
* field, it's the offset from the start of the object, if
156156
* it's static, it's from the start of the memory chunk
157157
* allocated for statics for the class.
158-
* For special static fields, this is set to -1 during vtable construction.
158+
* -1 means its a special static field.
159+
* -2 means its a collectible static field.
159160
*/
160161
int offset;
161162
};
@@ -360,6 +361,7 @@ struct MonoVTable {
360361
MonoDomain *domain; /* each object/vtable belongs to exactly one domain */
361362
gpointer type; /* System.Type type for klass */
362363
guint8 *interface_bitmap;
364+
MonoGCHandle loader_alloc; /* LoaderAllocator object for objects in collectible alcs */
363365
guint32 max_interface_id;
364366
guint8 rank;
365367
/* Keep this a guint8, the jit depends on it */

src/mono/mono/metadata/icall-decl.h

+2
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,6 @@ ICALL_EXPORT MonoBoolean ves_icall_System_Array_IsValueOfElementTypeInternal (Mo
208208

209209
ICALL_EXPORT MonoBoolean ves_icall_System_Array_FastCopy (MonoObjectHandleOnStack source_handle, int source_idx, MonoObjectHandleOnStack dest_handle, int dest_idx, int length);
210210

211+
ICALL_EXPORT MonoBoolean ves_icall_System_Reflection_LoaderAllocatorScout_Destroy (gpointer native);
212+
211213
#endif // __MONO_METADATA_ICALL_DECL_H__

0 commit comments

Comments
 (0)