Экспериментируя с WeakHandles, я столкнулся с этой особенностью в .NET 6.
static void Main(string[] args) {
var foo = new int[3];
var fooWeakHandle = GCHandle.Alloc(foo, GCHandleType.Weak);
GC.Collect();
Console.WriteLine(fooWeakHandle.Target);
}
Результат с .NET 6 (скомпилированный в режиме Release, чтобы избежать нетерпеливого сбора корней) оказался на удивление
System.Int32[]
В .NET Framework 4.7.2 это значение null/nothing в Release и System.Int32[] в Debug. Я получил те же результаты с LinqPad (оптимизировать +-) для Framework и .NET (Core).
Почему не собирается массив, на который ссылается только мусор WeakHandle?
Обновлено:
Я попробовал, как было предложено, переместить код в другой метод - все тот же результат (даже добавил немного ненужных опций MethodImpl)
IL одинаков для .NET Framework 4.7.2 и .NET 6 (разница только в mscorlib и System.Runtime). Даже добавил пустой класс, чтобы попробовать new B()
вместо нового int[]
... результаты те же.
static void Main(string[] args) {
DifferentFunction();
}
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
static void DifferentFunction() {
var foo = new int[3];
var fooWeakHandle = GCHandle.Alloc(foo, GCHandleType.Weak);
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
Console.WriteLine(fooWeakHandle.Target);
}
class B
{
}
.method private hidebysig static
void DifferentFunction () cil managed noinlining nooptimization
{
// Method begins at RVA 0x2058
// Header size: 12
// Code size: 35 (0x23)
.maxstack 4
.locals init (
[0] valuetype [System.Runtime]System.Runtime.InteropServices.GCHandle fooWeakHandle
)
// GCHandle gCHandle = GCHandle.Alloc(new int[3], GCHandleType.Weak);
IL_0000: ldc.i4.3
IL_0001: newarr [System.Runtime]System.Int32
IL_0006: ldc.i4.0
IL_0007: call valuetype [System.Runtime]System.Runtime.InteropServices.GCHandle [System.Runtime]System.Runtime.InteropServices.GCHandle::Alloc(object, valuetype [System.Runtime]System.Runtime.InteropServices.GCHandleType)
IL_000c: stloc.0
// GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
IL_000d: ldc.i4.2
IL_000e: ldc.i4.1
IL_000f: ldc.i4.1
IL_0010: ldc.i4.1
IL_0011: call void [System.Runtime]System.GC::Collect(int32, valuetype [System.Runtime]System.GCCollectionMode, bool, bool)
// Console.WriteLine(gCHandle.Target);
IL_0016: ldloca.s 0
IL_0018: call instance object [System.Runtime]System.Runtime.InteropServices.GCHandle::get_Target()
IL_001d: call void [System.Console]System.Console::WriteLine(object)
// }
IL_0022: ret
} // end of method Program::DifferentFunction
Спасибо за комментарий. Я отредактировал вопрос для ясности: почему не собирается массив, на который ссылается только WeakHandle?
@tinmanjk вы сравнивали сгенерированные IL с помощью ILSpy или аналогичного? Технически foo
входит в объем, поэтому вполне понятно, что его не заберут, пока вы не уйдете Main
@zaitsman это в лексической области, но нет ссылки на него после GC.Collect(), поэтому его следует собрать.
Why isn't the array that is only referenced by the WeakHandle garbage collected
Какая документация заставляет вас думать, что это должно быть?
@tinmanjk, поэтому я спросил тебя, сравнивали ли вы IL. Я поддерживаю [Рене] в этом вопросе, почти уверен, что никогда не было гарантировано, что это будет собрано, это может быть конкретная реализация ILGen в старом .Net и ядре ИЛИ это может быть даже поведение конкретной операции GC.
Если вы вынесете массив и создание GCHandle в отдельную функцию (например, var fooWeakHandle = GetWeakHandle();
), то массив будет собран так, как вы ожидаете в net6.
Отвечает ли это на ваш вопрос? Я вообще не менял свой код, почему я наблюдаю регрессию в памяти при обновлении версии .NET?
@Шинго, спасибо. Это было очень информативно. «Это потому, что, когда JIT генерирует для вас код, он может свободно продлить время жизни до конца метода. Таким образом, в этом случае JIT просто продлил время жизни o до конца метода Main, поэтому GC.Collect(), который вызывает сборщик мусора с полной блокировкой не может собирать o. Чтобы избежать этого, вы можете использовать o в отдельном методе и отключить встраивание». Это определенно противоречит моей текущей ментальной модели, но я думаю, что JIT может делать что угодно (частично и частично). полностью прерываемые методы, похоже, не имеют значения)
В .net framework foo
можно собирать после последнего использования. GCHandle.Alloc(foo, GCHandleType.Weak)
— это последнее использование, поэтому его можно забрать в GC.Collect()
.
Это может вызвать проблемы при выполнении собственного взаимодействия, поскольку объект может владеть собственными ресурсами, а сборщик мусора не может знать, используются ли такие ресурсы собственным кодом или нет, и это может привести к преждевременному удалению ресурсов. И это может быть довольно неожиданно, если вы не знаете об этой особенности GC, но ее можно исправить с помощью GC.KeepAlive.
Это поведение, очевидно, было изменено в более поздних версиях .net, но я не видел ни одного источника о точном поведении. Я не знаю точной причины этого изменения, но предполагаю, что риск возникновения ошибок перевешивает незначительное улучшение производительности сбора данных.
Так что на самом деле это не изменение слабой ссылки, а скорее изменение того, когда объекты считаются коллекционируемыми.
Да, похоже, что JIT другой и не так стремится помечать объекты как недоступные. «Предполагаем, что объекты должны оставаться активными, пока любая ссылка находится в области видимости», хотя это неверно... Я только что добавил фиктивный цикл в метод for (int i = 0; i < 1; i++) { }, и поведение то же, что и в .NET Framework.
@tinmanjk Я нисколько не удивлен, что реальное поведение намного сложнее, чем я описал. Возможно, он отслеживает время жизни ссылки для каждого блока кода?
Обновлено: я нашел еще один ответ, который более точно отражает разницу между .NET Framework и .NET Core - многоуровневая компиляция.
Это включено по умолчанию в .NET 5 (и .NET Core 3+), но не доступен в .NET 4.8.
В вашем случае результатом является то, что ваш метод скомпилирован с указанным «быстрая» компиляция и недостаточно оптимизирована для работы вашего кода. как вы и ожидаете (то есть время жизни переменной myObject продлевается до тех пор, пока не будет конец метода). Это так, даже если вы компилируете в Release режим с включенной оптимизацией и без подключенного отладчика.
Когда я компилирую свой метод с помощью
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
ожидаемые вещи происходят. Однако поскольку ответ содержал некоторые оговорки
многоуровневая компиляция будет (для некоторых методов при некоторых условиях) сначала скомпилируйте сырую, низкооптимизированную версию метода, а затем при необходимости подготовим более оптимизированную версию.
Я оставлю свой старый ответ, в котором описаны полностью и частично прерываемые методы как возможная причина разницы и главный вывод для меня, который
в то время как компиляция в режиме отладки всегда продлит время жизни локальные переменные в конец метода, метка Release Mode CAN переменные (корни) мертвы (больше не доступны), если JIT-код эвристика решила, что это выгодно. Но это не всегда дело, как это доказано.
СТАРЫЙ ответ:
Хотя ответ Джонаша указывает в правильном направлении, я хотел бы изложить детали, которые мне удалось найти в ходе расследований, которые могут представлять интерес.
Похоже, что ключом ко всему этому является то, что методы могут быть частично или полностью прерываемыми (из этого ответа):
Однако информация о живучести корней стека сохраняется только для так называемые безопасные точки. Существует два типа методов: частично прерываемый — единственные безопасные точки — во время вызовов других методов. Это делает метод менее «приостанавливаемым», поскольку среде выполнения необходимо дождаться такой безопасной точки, чтобы приостановить метод, но потребляет меньше памяти для информации GC. полностью прерываемая — каждая инструкция метода рассматривается как безопасная точка, что, очевидно, делает метод очень «приостанавливаемым», но требует значительного объема памяти (по количеству, аналогичному самому коду)
Как сказано в Book Of The Runtime: «JIT решает, выпускать ли полностью или частично прерываемый код, основанный на эвристике для поиска лучшего компромисс между качеством кода, размером информации GC и приостановкой GC задержка."
Я изменил свой первоначальный пример, чтобы проверить это (перенес код в другой метод, отсутствие встраивания является артефактом тестирования и не должно иметь значения). Также заменил массив фиктивным экземпляром класса на тот случай, если массивы будут более особенными.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace ConsoleApp2
{
internal class Program
{
static GCHandle fooWeakHandle;
static void Main()
{
DifferentFunction();
//GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
Console.WriteLine(fooWeakHandle.Target);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void DifferentFunction()
{
//var foo = new int[3];
var foo = new B();
// just having a loop switches to expected behavior
//for (int i = 0; i < 1; i++) { }
fooWeakHandle = GCHandle.Alloc(foo, GCHandleType.Weak);
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
}
class B
{
}
}
}
Теперь, если мы раскомментируем //for (int i = 0; i < 1; i++) { }
, мы получим полностью прерываемый метод, о чем свидетельствует вывод команды отладки WinDbg+sos !u -gcinfo 00007FFECDF682C7
**00000018 interruptible**
00007ffe`1f538298 488d4d88 lea rcx,[rbp-78h]
00007ffe`1f53829c 498bd2 mov rdx,r10
00007ffe`1f53829f e82cf8b05f call coreclr!JIT_InitPInvokeFrame (00007ffe`7f047ad0)
00007ffe`1f5382a4 488bf0 mov rsi,rax
00007ffe`1f5382a7 488bcc mov rcx,rsp
00007ffe`1f5382aa 48894da8 mov qword ptr [rbp-58h],rcx
00007ffe`1f5382ae 488bcd mov rcx,rbp
00007ffe`1f5382b1 48894db8 mov qword ptr [rbp-48h],rcx
00007ffe`1f5382b5 cc int 3
00007ffe`1f5382b6 b9b040651f mov ecx,1F6540B0h
00007ffe`1f5382bb fe ???
00007ffe`1f5382bc 7f00 jg 00007ffe`1f5382be
00007ffe`1f5382be 00e8 add al,ch
00007ffe`1f5382c0 3c33 cmp al,33h
00007ffe`1f5382c2 b95f488bc8 mov ecx,0C88B485Fh
D:\ConsoleApp2\ConsoleApp2\Program.cs @ 27:
**00000044 +rax**
**00000047 +rcx**
00007ffe`1f5382c7 33d2 xor edx,edx
00007ffe`1f5382c9 e822dab55f call coreclr!MarshalNative::GCHandleInternalAlloc (00007ffe`7f095cf0)
**0000004e -rcx -rax**
00007ffe`1f5382ce 488bf8 mov rdi,rax
00007ffe`1f5382d1 48b928af5c1ffe7f0000 mov rcx,7FFE1F5CAF28h
00007ffe`1f5382db ba01000000 mov edx,1
00007ffe`1f5382e0 e8fb34b95f call coreclr!JIT_GetSharedNonGCStaticBase_SingleAppDomain (00007ffe`7f0cb7e0)
00007ffe`1f5382e5 48b9902e22ea2c020000 mov rcx,22CEA222E90h
00007ffe`1f5382ef 488b09 mov rcx,qword ptr [rcx]
**00000072 +rcx**
00007ffe`1f5382f2 48897908 mov qword ptr [rcx+8],rdi
D:\ConsoleApp2\ConsoleApp2\Program.cs @ 29:
00007ffe`1f5382f6 b902000000 mov ecx,2
**0000007b -rcx**
00007ffe`1f5382fb ba0a000000 mov edx,0Ah
**00007ffe`1f538300 48b848f35e1ffe7f0000 mov rax,7FFE1F5EF348h (MD: System.GC._Collect(Int32, Int32))**
Я отметил ** интересные строки, где может происходить отслеживание.
Если мы прокомментируем фиктивный цикл, мы, похоже, получим частично прерываемый метод:
00007ffe`cdf68298 488d4d88 lea rcx,[rbp-78h]
00007ffe`cdf6829c 498bd2 mov rdx,r10
00007ffe`cdf6829f e82cf8af5f call coreclr!JIT_InitPInvokeFrame (00007fff`2da67ad0)
00007ffe`cdf682a4 488bf0 mov rsi,rax
00007ffe`cdf682a7 488bcc mov rcx,rsp
00007ffe`cdf682aa 48894da8 mov qword ptr [rbp-58h],rcx
00007ffe`cdf682ae 488bcd mov rcx,rbp
00007ffe`cdf682b1 48894db8 mov qword ptr [rbp-48h],rcx
00007ffe`cdf682b5 cc int 3
00007ffe`cdf682b6 b9b04008ce mov ecx,0CE0840B0h
00007ffe`cdf682bb fe ???
00007ffe`cdf682bc 7f00 jg 00007ffe`cdf682be
00007ffe`cdf682be 00e8 add al,ch
00007ffe`cdf682c0 3c33 cmp al,33h
00007ffe`cdf682c2 b85f488bc8 mov eax,0C88B485Fh
D:\ConsoleApp2\ConsoleApp2\Program.cs @ 27:
**00000044 is a safepoint:**
00007ffe`cdf682c7 33d2 xor edx,edx
00007ffe`cdf682c9 e822dab45f call coreclr!MarshalNative::GCHandleInternalAlloc (00007fff`2dab5cf0)
**0000004e is a safepoint:**
00007ffe`cdf682ce 488bf8 mov rdi,rax
00007ffe`cdf682d1 48b928afffcdfe7f0000 mov rcx,7FFECDFFAF28h
00007ffe`cdf682db ba01000000 mov edx,1
00007ffe`cdf682e0 e8fb34b85f call coreclr!JIT_GetSharedNonGCStaticBase_SingleAppDomain (00007fff`2daeb7e0)
**00000065 is a safepoint:**
00007ffe`cdf682e5 48b9902e8a4c6a020000 mov rcx,26A4C8A2E90h
00007ffe`cdf682ef 488b09 mov rcx,qword ptr [rcx]
00007ffe`cdf682f2 48897908 mov qword ptr [rcx+8],rdi
D:\ConsoleApp2\ConsoleApp2\Program.cs @ 29:
00007ffe`cdf682f6 b902000000 mov ecx,2
00007ffe`cdf682fb ba0a000000 mov edx,0Ah
00007ffe`cdf68300 48b848f301cefe7f0000 mov rax,7FFECE01F348h (MD: System.GC._Collect(Int32, Int32))
Я использовал Windbg+Sos всего несколько раз, но, основываясь на этом превосходном видео, я ожидаю, что безопасные точки также будут включать информацию +rax, -rax для отслеживания того, что активно, а что нет. Но в моем выводе это не так.
Так что в моем случае это обычно НЕ должно включать в себя какие-либо живые корни. Может быть, существует другой механизм поверх точек безопасности, который поддерживает объект в рабочем состоянии? Может быть, кто-нибудь сможет прокомментировать.
Главный вывод для меня заключается в том, что, хотя компиляция в режиме отладки всегда продлевает время жизни локальных переменных до конца метода, режим выпуска МОЖЕТ помечать переменные (корни) как мертвые (больше не доступные), если эвристика JIT-кода решила, что это полезно. . Но это не всегда так, как показывают факты.
За очень хорошую ссылку на этот точный вопрос главного мэйнтанера GC Маони (из комментариев к моему вопросу):
"This is because when JIT generates code for you, it's free to lengthen the lifetime till end of the method."
Я не думаю, что это является договором, что слабая ручка собирается при запуске GC. Он может освободить ссылку при запуске, но это может быть деталью реализации.