中年システムエンジニアのオモチャ箱

中年システムエンジニアが初体験のブログ活動。技術情報の備忘録以外も、色々と載せていければと思います。


VB.NET メモリリークによるメモリ解放の重要性(DataTable 編)


【スポンサーリンク】


前回は下記で、MemoryStream オブジェクトについて記載しました。

www.ma-se.com


今回は、DataTable オブジェクトについてのお話です。
私の実務では、ほぼデータベースを使用するシステム開発なので、DataTable オブジェクトの方が馴染みがあるのです!

調査するための基本ロジック

下記は今回のメモリリーク調査で作成したロジックです。
約 1GB のメモリを消費していました。
尚、「COL_A」~「COL_C」までの 3 列追加、及び、800 万回のループでの行追加は、テストでメモリ消費をさせたいだけなので特に意味はありません。

 

ソースコード

Private Sub TestMemLeakBase()

    Dim objRow As DataRow

    Dim objDt As New DataTable()

    objDt.Columns.Add("COL_A")

    objDt.Columns.Add("COL_B")

    objDt.Columns.Add("COL_C")

    For i As Integer = 1 To 8000000

        objRow = objDt.NewRow()

        objRow.Item("COL_A") = "TEST1"

        objRow.Item("COL_B") = "TEST2"

        objRow.Item("COL_C") = "TEST3"

        objDt.Rows.Add(objRow)

    Next

End Sub

メモリ解放を考慮していないロジック 

では、調査するための基本ロジックを踏まえて、下記はメモリ解放の対策をしていないロジックです。
BtnDtTest1 を押下し、TestDt1 メソッドを実行しても問題なく動作します。
BtnDtTest1 を何度押下しても(=TestDt1 メソッドを何度実行しても)問題ないはずです。

 

ソースコード

Private Sub BtnDtTest1_Click(sender As Object, e As EventArgs) Handles BtnDtTest1.Click

    Call Me.TestDt1()

End Sub

 

Private Sub TestDt1()

    Dim objRow As DataRow

    Dim objDt1 As New DataTable()

    objDt1.Columns.Add("COL_A")

    objDt1.Columns.Add("COL_B")

    objDt1.Columns.Add("COL_C")

    For i As Integer = 1 To 8000000

        objRow = objDt1.NewRow()

        objRow.Item("COL_A") = "TEST1"

        objRow.Item("COL_B") = "TEST2"

        objRow.Item("COL_C") = "TEST3"

        objDt1.Rows.Add(objRow)

    Next

End Sub

メモリ解放処理をしていなくてもメモリリークが発生しない理由は、TestDt1 メソッドの呼び出しが完了した段階で、.NET 側でメモリ解放処理(ガベージコレクションの発動)を暗黙的におこなっているからです。 

 

では、下記のように BtnDtTest1 を 2 回押下した動作と同じロジックを実行した場合は、どうなるでしょう?

 

ソースコード

Private Sub BtnDtTest1_Click(sender As Object, e As EventArgs) Handles BtnDtTest1.Click

    Call Me.TestDt1()

End Sub

 

Private Sub TestDt1()

    Dim objRow As DataRow

 

    '1 回目の呼び出し

    Dim objDt1 As New DataTable()

    objDt1.Columns.Add("COL_A")

    objDt1.Columns.Add("COL_B")

    objDt1.Columns.Add("COL_C")

    For i As Integer = 1 To 8000000

        objRow = objDt1.NewRow()

        objRow.Item("COL_A") = "TEST1"

        objRow.Item("COL_B") = "TEST2"

        objRow.Item("COL_C") = "TEST3"

        objDt1.Rows.Add(objRow)

    Next

 

    '2 回目の呼び出し

    Dim objDt2 As New DataTable()

    objDt2.Columns.Add("COL_A")

    objDt2.Columns.Add("COL_B")

    objDt2.Columns.Add("COL_C")

    For i As Integer = 1 To 8000000

        objRow = objDt2.NewRow()

        objRow.Item("COL_A") = "TEST1"

        objRow.Item("COL_B") = "TEST2"

        objRow.Item("COL_C") = "TEST3"

        objDt2.Rows.Add(objRow)

    Next

End Sub

結果は、途中で Out Of Memoty のエラーで落ちますよね。
BtnDtTest1 を 2 回押下した動作と同じ処理を行っているのに、何故、連続で同じロジックを実行するとエラーが発生するのでしょうか?
理由は、obj1Dt1 のメモリ解放処理を実施せずに、同一メソッド内で objDt2 の生成でメモリ消費をしているためです。
BtnDtTest1 を 2 回押下した場合は、TestDt1 メソッドを 2 回呼び出していたので(暗黙的にメモリ解放を行っていたので)、問題なく動作しました。

これは悪い例なので、マネしないようにしてください。

 

 
メモリ解放を考慮したロジック

では、メモリリークが発生したロジックに、メモリ解放処理を入れてみましょう!

  1. 使用済みになった後に、Nothing をセットする。
    または、Dispose メソッドが呼び出せる場合は、Dispose メソッド呼び出し後、Nothing をセットする。
    ネットで調べると、大体上記の事が出てくると思います。

    ソースコード

    Private Sub BtnDtTest1_Click(sender As Object, e As EventArgs) Handles BtnDtTest1.Click

        Call Me.TestMem1()

    End Sub

     

    Private Sub TestDt1()

        Dim objRow As DataRow

     

        '1 回目の呼び出し

        Dim objDt1 As New DataTable()

        objDt1.Columns.Add("COL_A")

        objDt1.Columns.Add("COL_B")

        objDt1.Columns.Add("COL_C")

        For i As Integer = 1 To 8000000

            objRow = objDt1.NewRow()

            objRow.Item("COL_A") = "TEST1"

            objRow.Item("COL_B") = "TEST2"

            objRow.Item("COL_C") = "TEST3"

            objDt1.Rows.Add(objRow)

        Next

        objDt1.Dispose()

        objDt1 = Nothing

     

        '2 回目の呼び出し

        Dim objDt2 As New DataTable()

        objDt2.Columns.Add("COL_A")

        objDt2.Columns.Add("COL_B")

        objDt2.Columns.Add("COL_C")

        For i As Integer = 1 To 8000000

            objRow = objDt2.NewRow()

            objRow.Item("COL_A") = "TEST1"

            objRow.Item("COL_B") = "TEST2"

            objRow.Item("COL_C") = "TEST3"

            objDt2.Rows.Add(objRow)

        Next

        objDt2.Dispose()

        objDt2 = Nothing

    End Sub

    VB.NET メモリリークによるメモリ解放の重要性(MemoryStream 編)に引き続き、また予期せぬ Out Of Memoty のエラーが発生しました。
    色々と調べてみると、DataTable オブジェクトでは Clear メソッドで、リソースが解放されるみたいです。
    では、Dispose メソッドを、Clear メソッドに変更してみましょう!

    ソースコード

    Private Sub BtnDtTest1_Click(sender As Object, e As EventArgs) Handles BtnDtTest1.Click

        Call Me.TestMem1()

    End Sub

     

    Private Sub TestDt1()

        Dim objRow As DataRow

     

        '1 回目の呼び出し

        Dim objDt1 As New DataTable()

        objDt1.Columns.Add("COL_A")

        objDt1.Columns.Add("COL_B")

        objDt1.Columns.Add("COL_C")

        For i As Integer = 1 To 8000000

            objRow = objDt1.NewRow()

            objRow.Item("COL_A") = "TEST1"

            objRow.Item("COL_B") = "TEST2"

            objRow.Item("COL_C") = "TEST3"

            objDt1.Rows.Add(objRow)

        Next

        objDt1.Dispose()  objDt1.Clear()

        objDt1 = Nothing

     

        '2 回目の呼び出し

        Dim objDt2 As New DataTable()

        objDt2.Columns.Add("COL_A")

        objDt2.Columns.Add("COL_B")

        objDt2.Columns.Add("COL_C")

        For i As Integer = 1 To 8000000

            objRow = objDt2.NewRow()

            objRow.Item("COL_A") = "TEST1"

            objRow.Item("COL_B") = "TEST2"

            objRow.Item("COL_C") = "TEST3"

            objDt2.Rows.Add(objRow)

        Next

        objDt2.Dispose()  objDt2.Clear()

        objDt2 = Nothing

    End Sub

     これで、メモリリークが発生せずに実行できましたね!

  2. Using を使用する。
    ネットで調べると、Using を使用することにより、リソースが解放されるとの事が出てくると思います。

    VB.NET メモリリークによるメモリ解放の重要性(MemoryStream 編)では、Using を使用することはできませんでした。

    DataTable オブジェクトではどうでしょうか?

    ソースコード

    Private Sub BtnDtTest2_Click(sender As Object, e As EventArgs) Handles BtnDtTest2.Click

        Call Me.TestDt2()

    End Sub

     

    Private Sub TestDt2()

        Dim objRow As DataRow

     

        '1 回目の呼び出し

        Using objDt1 As New DataTable()

            objDt1.Columns.Add("COL_A")

            objDt1.Columns.Add("COL_B")

            objDt1.Columns.Add("COL_C")

            For i As Integer = 1 To 8000000

                objRow = objDt1.NewRow()

                objRow.Item("COL_A") = "TEST1"

                objRow.Item("COL_B") = "TEST2"

                objRow.Item("COL_C") = "TEST3"

                objDt1.Rows.Add(objRow)

            Next

        End Using

     

        '2 回目の呼び出し

        Using objDt2 As New DataTable()

            objDt2.Columns.Add("COL_A")

            objDt2.Columns.Add("COL_B")

            objDt2.Columns.Add("COL_C")

            For i As Integer = 1 To 8000000

                objRow = objDt2.NewRow()

                objRow.Item("COL_A") = "TEST1"

                objRow.Item("COL_B") = "TEST2"

                objRow.Item("COL_C") = "TEST3"

                objDt2.Rows.Add(objRow)

            Next

        End Using

    End Sub

    結果は、VB.NET メモリリークによるメモリ解放の重要性(MemoryStream 編)と同様に、途中で Out Of Memoty のエラーで落ちちゃいました…。
    VB.NET メモリリークによるメモリ解放の重要性(MemoryStream 編)の時と同様に、Using は使用できないのかな??
    と思ったのですが、Using で終了する前に、DataTable オブジェクトの Clear メソッドを呼び出すことによりメモリ解放することができました!

    ソースコード

    Private Sub BtnDtTest2_Click(sender As Object, e As EventArgs) Handles BtnDtTest2.Click

        Call Me.TestDt2()

    End Sub

     

    Private Sub TestDt2()

        Dim objRow As DataRow

     

        '1 回目の呼び出し

        Using objDt1 As New DataTable()

            objDt1.Columns.Add("COL_A")

            objDt1.Columns.Add("COL_B")

            objDt1.Columns.Add("COL_C")

            For i As Integer = 1 To 8000000

                objRow = objDt1.NewRow()

                objRow.Item("COL_A") = "TEST1"

                objRow.Item("COL_B") = "TEST2"

                objRow.Item("COL_C") = "TEST3"

                objDt1.Rows.Add(objRow)

            Next

            objDt1.Clear()

        End Using

     

        '2 回目の呼び出し

        Using objDt2 As New DataTable()

            objDt2.Columns.Add("COL_A")

            objDt2.Columns.Add("COL_B")

            objDt2.Columns.Add("COL_C")

            For i As Integer = 1 To 8000000

                objRow = objDt2.NewRow()

                objRow.Item("COL_A") = "TEST1"

                objRow.Item("COL_B") = "TEST2"

                objRow.Item("COL_C") = "TEST3"

                objDt2.Rows.Add(objRow)

            Next

            objDt2.Clear()

        End Using

    End Sub

     

VB.NET メモリリークによるメモリ解放の重要性(MemoryStream 編)とは、若干、結果が異なりましたね。
Using を使用しても、完全にリソースが解放されない事には疑問が残りますが…。

 

オブジェクトにより、リソースの解放方法(Dispose、Clear、Close など)は異なりました。
しかしリソースを解放し、ガベージコレクションの処理対象(Using を使用や、Nothing のセット)にすれば、メモリリーク対策になるという事を忘れずに!


Visual Basicの絵本 Windowsプログラミングわかる9つの扉