前回は下記で、MemoryStream オブジェクトについて記載しました。
今回は、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 回呼び出していたので(暗黙的にメモリ解放を行っていたので)、問題なく動作しました。
これは悪い例なので、マネしないようにしてください。
メモリ解放を考慮したロジック
では、メモリリークが発生したロジックに、メモリ解放処理を入れてみましょう!
- 使用済みになった後に、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
これで、メモリリークが発生せずに実行できましたね!
- 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 のセット)にすれば、メモリリーク対策になるという事を忘れずに!