呼ばれないファイナライザー

.NET 5 / C#9.0

公式ページでの解説

※この記事は2020年12月に書いています。

.NETの開発者向けの公式ドキュメントの記事はこちら

ファイナライザー - C# プログラミング ガイド - C#
ガベージ コレクターによってクラス インスタンスが収集されている場合は、C# のファイナライザーによって、最終的に必要なすべてのクリーンアップが実行されます。
Object.Finalize メソッド (System)
オブジェクトが、ガベージ コレクションによって収集される前に、リソースの解放とその他のクリーンアップ操作の実行を試みることができるようにします。

一部引用すると

ガベージ コレクターによってクラス インスタンスが収集されている場合は、ファイナライザー (デストラクターとも呼ばれます) を使用して、最終的に必要なすべてのクリーンアップが実行されます。

とありますが、そうでもないようです。

.NET Frameworkの場合は良いのですが、.NET Coreや.NET 5では、プロセス終了時に未実行のファイナライザーは実行されません。

サンプルコードの実行結果

上記ページにサンプルコードがあります。

using System;
using System.Diagnostics;

public class ExampleClass
{
Stopwatch sw;

public ExampleClass()
{
sw = Stopwatch.StartNew();
Console.WriteLine("Instantiated object");
}

public void ShowDuration()
{
Console.WriteLine("This instance of {0} has been in existence for {1}",this, sw.Elapsed);
}

~ExampleClass()
{
Console.WriteLine("Finalizing object");
sw.Stop();
Console.WriteLine("This instance of {0} has been in existence for {1}",this, sw.Elapsed);
}
}

public class Demo
{
public static void Main()
{
ExampleClass ex = new ExampleClass();
ex.ShowDuration();
}
}

これを.NET Framework 上で実行すると記事にあるとおり以下のような結果になります。

Instantiated object
This instance of ExampleClass has been in existence for 00:00:00.0008002
Finalizing object
This instance of ExampleClass has been in existence for 00:00:00.0020109

同じコードを.NET 5 上で実行すると以下のような結果になります。

Instantiated object
This instance of ExampleClass has been in existence for 00:00:00.0144380

つまり、ファイナライザーが実行されていません。

GitHub上での議論

API review: During shutdown, revisit finalization and provide a way to clean up resources · Issue #16028 · dotnet/runtime
Running finalizers on reachable objects during shutdown is currently unreliable. This is a proposal to fix that and prov...

検索すると上記のようにファイナライザーに関する議論がいくつかみつかります。

そこではファイナライザーの代替としてアセンブリのアンロードイベントの使用について述べられています。

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

namespace ConsoleApp1103
{
class Program
{
static void Main(string[] args)
{
var tester = new Tester();

var current = Assembly.GetExecutingAssembly();

AssemblyLoadContext.GetLoadContext(current).Unloading += sender =>
{
Console.WriteLine("コンテクストアンロード");
};

GC.Collect();

tester = null;

GC.Collect();

GC.Collect();

}
}

public class Tester
{
public Tester() => Console.WriteLine("コンストラクタ");

~Tester() => Console.WriteLine("ファイナライザー");

}
}

このコードを実行すると

コンストラクタ
コンテクストアンロード

となります。

もう少し詳しく

プロセス実行中に.NETによるメモリ開放処理は.NET Frameworkと同様に.NET Coreや.NET 5でも実行されます。

プロセス終了時に未実行のファイナライザーは.NET Frameworkでは呼ばれますが、.NET Coreや.NET 5では呼ばれません。

WPFの画面にボタンを一つ張り付けたもののコードビハインドです。

using System.Diagnostics;
using System.Reflection;
using System.Windows;

namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();

Debug.WriteLine("スタート");

var current = Assembly.GetExecutingAssembly();

AssemblyLoadContext.GetLoadContext(current).Unloading += sender =>
{
Debug.WriteLine("アプリケーション終了");
};
}

private int count = 0;

private void Button_Click(object sender, RoutedEventArgs e)
{
var tester = new Tester(++count);

tester = null;
}

public class Tester
{
private int _id;

public Tester(int id)
{
_id = id;

Debug.WriteLine($"☆コンストラクタ {_id}");
}

~Tester() => Debug.WriteLine($"★ファイナライザー {_id}");

}
}
}

※上記は.NET 5 のコードです。.NET FrameworkではAssemblyLoadContextがないのでApplication.Exitイベントで代替して確認しました。

上のコードはボタンを押下する毎にTesterクラスを生成し、そのインスタンスへの参照を切ることによりGC可能とします。

.NET Frameworkで実行すると

☆コンストラクタ 1
☆コンストラクタ 2
☆コンストラクタ 3
☆コンストラクタ 4
☆コンストラクタ 5
★ファイナライザー 5
★ファイナライザー 4
★ファイナライザー 3
★ファイナライザー 2
★ファイナライザー 1
☆コンストラクタ 6
☆コンストラクタ 7
☆コンストラクタ 8
アプリケーション終了
★ファイナライザー 8
★ファイナライザー 7
★ファイナライザー 6

のようなコードが出力されます。8回押下してインスタンスを8つ作る途中で一度ファイナライザーが実行され(される場合とされない場合があります。実行契機は完全に.NET Frameworkお任せです)、×ボタンで画面終了させる後に未実行のファイナライザーが3つ実行されています。

これを.NET 5 で実行すると

☆コンストラクタ 1
☆コンストラクタ 2
☆コンストラクタ 3
★ファイナライザー 3
★ファイナライザー 2
★ファイナライザー 1
☆コンストラクタ 4
☆コンストラクタ 5
☆コンストラクタ 6
☆コンストラクタ 7
☆コンストラクタ 8
アプリケーション終了

ファイナライザーの実行契機はお任せですが、終了時に未実行分が実行されていません。