VB のたまご

作成日: 2017/03/27, 更新日: 2017/03/27


イベントに関するメモリリークを学ぶ1

メモリリークとは何か?

  •  (長いです。前置きなので、飛ばして構いません)。
  •  メモリリークとは、プログラムが大量のメモリを使用することで、メモリ使用量が許容量を超えて、リーク(Leak、漏れる)してしまう状態を言います。 これは、コップに水を注ぎすぎて、コップに収まりきらない分が溢れ漏れてしまう状態と似ています。

  •  .NET アプリでは、メモリリークすることは OutOfMemoryException 例外エラーが発生することを意味しています。 メモリリークは、【限度があるメモリの使用量を考慮しないような、誤った扱い方でプログラムを作りこんだ】ことによって発生するバグです。 限度を超えた、メモリの確保しすぎ・独占しすぎ、ということです。ちょっと厳しい言い方で書いてしまいましたが、メモリリークの原因はこういうことです。

  •  しかし、このことは、.NET アプリの利点である【土台となる .NET Framework がメモリ管理を代わりにやってくれるので、プログラマーはメモリ管理を気にしなくてもいい】という話と食い違うことになります。 メモリ周りは何とかしてくれるんじゃなかったの?と思ってしまいます。 結局、メモリ管理は自分でしないといけないのか、お任せしていいのか、よく分からなくなります。 今までメモリ管理はお任せしていたので、【実際どうなっているのかよく知らない】の段階で止まったままですし、【知らなくても良い】という認識でした。

  •  先程から何回も言っていますが、メモリリークの原因は【メモリの使い過ぎ】です。しかしこれが単純な、簡単な話ではありません。 例えば、比較用として、存在しないファイルを開こうとした時に発生する FileNotFoundException 例外エラーがあります。 この例外エラーが発生する原因となった場所は、「ファイルを開く」命令を書いている場所でしか発生しないのと、何らかのアクション、例えばボタンを押した後に発生する流れになることが多いと思いますので、 そんなに時間をかけずに解決できると思います。一応補足すると、そのプログラムに関する全ソースの範囲のうち、そのボタン処理を書いている限定的な範囲だけに絞り込めますよね。 よって、そんなに時間をかけずに原因個所の特定と対策をして、解決できると思います。

  •  しかし、メモリリークは違います。あっちの処理でもこっちの処理でも、いたるところでメモリは使います。それによって少しずつメモリ使用量が増加していきます。 (それぞれで確保したメモリが積み重なっていきます)。メモリは共有して使うものですので当然ですよね。だからって、メモリを使うことは悪いことではなくメモリを使わないとプログラムは動きません。 Windows でさえメモリを使って動いています。

  •  つまり、原因は Xxx の場所だ!という一点に絞れるものではなく、 全体を範囲に、中でもメモリを確保している部分を重点的に、処理の推移とメモリ使用量の増加量を推理しながら、見て調べていかないといけない、巧妙に仕組まれた複数の頭脳犯探しなのです。 調査に時間がかかる、対策考えるのに時間がかかる、直すのに時間がかかる。このように、メモリリークとの出会いは悲しみしかありません。

  •  この背景から、メモリリークを知って、どう対処すればいいのか、あらかじめ知っていた方が良いよね!と思って、今回の記事が始まります。 【現象が出てから対戦相手を知る】のではなく【現象が出る前に、対戦相手を知って、プログラムを現象が出にくい子に育てる】、 【現象が出たとしても、あらかじめ相手の特徴を知っていることで、立ち止まらずに今後の出方を考えられる】ように、なっていただければ、この記事の甲斐があるかなと思いました。

  •  ただしここでは、メモリリークのうち【イベントに関するメモリリーク】について取り上げて、見ていきたいと思います。 COM オブジェクト等のアンマネージリソースや、画像などのリソースを扱った場合のメモリリークについてはこの記事では扱っていません。メモリリークは何種類もあるんですね。

  • 以降のサンプルは GitHub にもアップしています。

  • GitHub
    https://github.com/sutefu7/LearnToMemoryLeakAboutEvent
    Src/ConsoleApplication0.ForWinForms.VB
    または
    Src/ConsoleApplication0.ForWinForms
    

    スポンサーリンク


実際にメモリリークを見てみよう

  •  いきなり【イベントに関するメモリリーク】に入る前に、とりあえず、メモリリークを見てみましょう。 以下は簡単に出せる【メモリ確保しすぎによるメモリリーク】の例です。C# / VB それぞれ記載しています。

  •  C# の場合

  • using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                // メモリリークを見てみよう
                try
                {
                    // 起動時のメモリ使用量
                    Console.WriteLine($"{GC.GetTotalMemory(false).ToString("N")} bytes");
    
                    var s = "abcdefg hkjklmn opqrstu vwxyz 1234567 890";
                    for (var i = 0; i < int.MaxValue; i++)
                    {
                        s += s;
                    }
    
                }
                catch (OutOfMemoryException ex)
                {
                    Console.WriteLine($"{ex.Message}メモリリークですね。");
                    // メモリリーク時のメモリ使用量
                    Console.WriteLine($"{GC.GetTotalMemory(false).ToString("N")} bytes");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"{ex.Message}");
                }
    
    
                Console.Read();
            }
        }
    }
    

  •  VB の場合

  • Module Module1
    
        Sub Main()
    
            ' メモリリークを見てみよう
            Try
    
                ' 起動時のメモリ使用量
                Console.WriteLine($"{GC.GetTotalMemory(False).ToString("N")} bytes")
    
                Dim s = "abcdefg hkjklmn opqrstu vwxyz 1234567 890"
                For i As Integer = 0 To Integer.MaxValue
    
                    s &= s
    
                Next
    
            Catch ex As OutOfMemoryException
    
                Console.WriteLine($"{ex.Message}メモリリークですね。")
                ' メモリリーク時のメモリ使用量
                Console.WriteLine($"{GC.GetTotalMemory(False).ToString("N")} bytes")
    
            Catch ex As Exception
    
                Console.WriteLine($"{ex.Message}")
    
            End Try
    
    
            Console.Read()
        End Sub
    
    End Module
    

    出力結果の一例
    617,612.00 bytes
    種類 'System.OutOfMemoryException' の例外がスローされました。メモリリークですね
    。
    344,048,792.00 bytes
    

  •  私の環境では、300 MB くらいで例外エラーが発生しました。 これは大量にメモリを確保しようとしたことが原因です。文字列を大量に扱う場合は、String 型ではなく StringBuilder 型を使いますね。 このサンプルは、【無限に近いメモリ確保を命令されて、命令通りに確保したことが原因】であり、悪いのは、そのように動くように命令したプログラマーです。

  • スポンサーリンク


  •  次の事例を見てみましょう。 ここでは、【土台となる .NET Framework がメモリ管理を代わりにやってくれるので、プログラマーはメモリ管理を気にしなくてもいい】の部分を知ることができます。

  •  C# の場合

  • using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication2
    {
        class Person
        {
            public string Name { get; set; } = "taro";
            public int Age { get; set; } = 32;
        }
    }
    

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication2
    {
        class Program
        {
            static void Main(string[] args)
            {
                // 普段、正しく?使っていれば、メモリリークは発生せず、GC(ガベージコレクション)が自動的に回収してくれる
                Console.WriteLine($"{(GC.GetTotalMemory(false) / 1024).ToString("N")} KB");
                Console.WriteLine();
    
                for (var i = 0; i < 10; i++)
                {
                    for (var k = 0; k < 50000; k++)
                    {
                        var obj = new Person();
                    }
                    Console.WriteLine($"{(GC.GetTotalMemory(false) / 1024).ToString("N")} KB");
                }
    
    
                Console.Read();
            }
        }
    }
    

  •  VB の場合

  • Public Class Person
    
        Public Property Name As String = "taro"
        Public Property Age As Integer = 32
    
    End Class
    

    Module Module1
    
        Sub Main()
    
            ' 普段、正しく?使っていれば、メモリリークは発生せず、GC(ガベージコレクション)が自動的に回収してくれる
            Console.WriteLine($"{(GC.GetTotalMemory(False) / 1024).ToString("N")} KB")
            Console.WriteLine()
    
            For i As Integer = 0 To 10
    
                For k As Integer = 0 To 50000
    
                    Dim obj = New Person
    
                Next
                Console.WriteLine($"{(GC.GetTotalMemory(False) / 1024).ToString("N")} KB")
    
            Next
    
    
            Console.Read()
        End Sub
    
    End Module
    

    出力結果
    523.00 KB
    
    1,313.00 KB
    2,089.00 KB
    2,873.00 KB
    641.00 KB
    1,425.00 KB
    2,201.00 KB
    2,985.00 KB
    697.00 KB
    1,473.00 KB
    2,257.00 KB
    

  •  データクラスを1つ用意して、無限とは言えませんが、かなりの数をインスタンス生成した例です。 出力結果を見てみましょう。どんどんとメモリ使用量が増加していきますが、ある一定値まで増加すると、次の値が減少しています。 .NET Framework が、不要になった確保中のメモリを開放していることが分かります。ちゃんと働いていますね。 このように、プログラマーが破棄処理を書かずとも、メモリリークが発生しないように守ってくれます。

  •  次回に続きます。

  • スポンサーリンク