コンカレントGc

1,784 views

Published on

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,784
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
13
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

コンカレントGc

  1. 1. <ul>dalvik の GC をのぞいてみた @akachochin </ul>
  2. 2. 自己紹介 <ul><li>名前:@akachochin
  3. 3. 某メーカーでMFPやプリンタにnetbsdなOSを組み込んで、飲み代稼いでいるひと。
  4. 4. どうにかこうにか、コーディング含めた開発業務にありつけています。
  5. 5. 趣味は名前のとおり、赤提灯めぐり。
  6. 6. 場末の呑み屋、さいこー。 </li></ul>
  7. 7. 今回GCを選んだわけ <ul><li>懇親会でうっかり「じゃあ、喋ってみようかな」と口走ったから(笑)
  8. 8. ネタ探しでWeb Pageみてたら「2.3からコンカレントGCをサポート」と書いてあった。
  9. 9. もともとOS屋でメモリ管理まわりは興味があった。
  10. 10. こうしたわけで、コンカレントGCを中心にGCのソースを読んでみたお話をしようと思いました。 </li></ul>
  11. 11. おさらい1 - GCとヒープ <ul><li>ヒープとは、プログラムが動作中に必要に応じてメモリが欲しくなったときに使うためのメモリ領域である。
  12. 12. プログラムが動的にメモリを確保するとき、ヒープからメモリを切り出して、プログラムに渡す。
  13. 13. GCとは「ヒープ上で使われなくなった領域を解放し、プログラムがヒープを再利用することができるようにする」プログラムのことである。 </li></ul>
  14. 14. おさらい2 - もしGCがなかったら <ul><li>GCがない環境でヒープが欲しくなった場合、明示的にヒープ割り当て要求(Ex.malloc())を行い、ヒープが必要なくなった時点で明示的に解放(free())する。
  15. 15. 人間は間違える生き物。free()を忘れたり、free()したヒープを再度free()したり、いろいろやらかす。
  16. 16. こういうバグは見つけるのがホントに大変。 </li></ul>
  17. 17. Dalvikの概要 <ul><li>Dalvikとは、AndoroidのGC。
  18. 18. 名著「ガベージコレクションのアルゴリズムと実装」によれば、基本はマーク&スイープなアルゴリズムで実装されている・・・らしい。
  19. 19. (けれどコンパイルオプション次第ではCopy GCも使えるらしいです。)
  20. 20. さて、ソースを読むか・・・の前に前提となる知識に軽く触れておきましょう。 </li></ul>
  21. 21. 前提1 -マーク&スイープ- <ul><li>使われなくなったヒープを解放するアルゴリズムの一つ
  22. 22. ヒープ内で使われているオブジェクトを探して印をつけます。 </li></ul>ヒープ内のオブジェクトの参照を順番にたどって、印をつけていき ( マークフェーズ ) ・・・ 印 印 印 印
  23. 23. 前提1 -マーク&スイープ- <ul><li>ヒープをサーチして、使われてないヒープを解放(スイープフェーズ)する。 </li></ul>ヒープを先頭からサーチし、印のついてないヒープを解放していきます。 ※ サーチの過程で「印」は消しますが、図のわかりやすさのため、「印」を残しています。 印 印 印 印 解放 解放 解放
  24. 24. 前提2 – ルート - <ul><li>先の「マークフェーズ」で「使われているオブジェクトを順番にたどって」と書きました。
  25. 25. たどるときの大元、先頭のオブジェクトを特に「ルート」と言います。例えばJavaのオブジェクトの場合、以下青の箇所がルートになります。 </li></ul>gDvm
  26. 26. GCをしているのはどこ? <ul><li>何を糸口に追っていこうか・・・。
  27. 27. 今回はコンカレントGCを追うわけだし。
  28. 28. grep -ri concurrent *
  29. 29. すると、以下の箇所を発見する。
  30. 30. vm/alloc/HeapSource.c: dvmCollectGarbageInternal(false, GC_CONCURRENT); </li></ul>
  31. 31. GCをしているのはどこ? <ul><li>見つけた箇所はstatic void *gcDaemonThread()という関数の中。
  32. 32. 調べてみると、どうもこいつはスレッドで、ヒープの空きが一定のしきい値を下回った時に動く様だ。
  33. 33. この関数は dvmCollectGarbageInternal() という関数を呼んでいる。名前から言ってもこいつが空きメモリを回収しているのだろう。 </li></ul>
  34. 34. <ul>dvmCollectGarbageInternal </ul><ul><li>うわ、長い(笑)
  35. 35. 第二引数はどうもこの関数を呼び出した状況(理由)を渡すものらしい。
  36. 36. 理由の一覧はvm/alloc/Heap.hのGcReasonで定義されている。 GC_CONCURRENT はそのうちの一つ。
  37. 37. ソースを眺めてみると、 GC を呼び出した理由によって処理を変えている。今回は GC_CONCURRENT なケースにスポットを当てて説明します。 </li></ul>
  38. 38. <ul>dvmCollectGarbageInternal </ul><ul><li>GC_CONCURRENT な場合・・・
  39. 39. 1.GC を行うスレッドの優先度を上げない
  40. 40. 2.GC の処理中に VM 内の他のスレッド ( ミューテータ ) が動けるようにする処理 (dvmResumeAllThreads(SUSPEND_FOR_GC);) がある。
  41. 41. -> 普通は GC を行う場合、 VM 内の他のスレッドがメモリの状態を変えて欲しくないので、 GC を行うスレッド以外の動きを止めたいはず。 </li></ul>
  42. 42. <ul>dvmCollectGarbageInternal </ul><ul><li>けれど、 13 ページに書いた 2 つの処理によって他のスレッドが動けるようになるため、通常の VM の様に「処理が固まる」割合が減る。
  43. 43. 通常の GC のイメージ
  44. 44. コンカレントな GC のイメージ </li></ul>通常処理 通常処理 GC 通常処理 通常処理 通常処理
  45. 45. ここで突然 <ul><li>ここで一旦 dvmCollectGarbageInternal を離れて、 メモリ割り当ての関数(dvmMalloc())を読んでみます。
  46. 46. いきなりマーク&スイープなGCの処理を読んでも「んんっ???」という感じになります。
  47. 47. まず、メモリ割り当てをする際に、どんなことをするのかGC的な視点からみてみましょう。 </li></ul>
  48. 48. dvmMalloc()について <ul><li>何らかのオブジェクトにメモリを割り当てる際にはdvmMalloc()を呼んでメモリを確保します。
  49. 49. 例えば、VM上で動くJavaプログラムがクラスをnewしてオブジェクトを作成する場合、VM側ではdvmAllocObject()からdvmMalloc()を呼び出して、オブジェクトのための領域を確保します。 </li></ul>
  50. 50. dvmMalloc() <ul><li>dvmMalloc() -> tryMalloc() ->dvmHeapSourceAlloc()をコールします。
  51. 51. この中で呼ばれている二つの関数
  52. 52. mspace_calloc()
  53. 53. countAllocation() ->dvmHeapBitmapSetObjectBit()
  54. 54. がとても重要です。mspace_calloc()は実際のメモリ割り当てを行います。
  55. 55. mspace_calloc()の説明は省略します。 </li></ul>
  56. 56. dvmHeapBitmapSetObjectBit() <ul><li>この関数呼び出しを行う上で第一引数についているliveBitsがとても重要です。
  57. 57. 次のページ以降でGCに関係するVM側のデータ構造をまとめてみます。 </li></ul>
  58. 58. GCに関係したデータ構造 GcHeap 構造体 グローバルなヒープに関する情報 HeapSource MarkContext GcMarkContext 構造体 現在やっているGCの状態 HeapSource 構造体 ヒープ空間に関する情報 liveBits markBits liveBits MarkContext bitmap stack finger この2つのビット マップがとても重要
  59. 59. bitmap系データ構造 ビットマップ (liveBits,MarkBits) の各ビットと「ヒープ内を 8byte ごとに区切った空間」がそれぞれ図のように対応している。 これによって、 8byte ごとの各ヒープ空間の状況を記録する。 liveBits MarkBits ヒープ領域 ひとつのビットは ヒープ8byte領域に 対応している ・・・・ ・・・・
  60. 60. dvmHeapBitmapSetObjectBit() <ul><li>割り当てたヒープ空間のうち、先頭8byteに当たる空間に対応したlivebitを立てる。
  61. 61. 例えば灰色のヒープ24byteを割り当てた場合はliveBitsは以下の様になる。 </li></ul>1 liveBits ヒープ領域 割り当てた領域の 先頭に対応した ビットを1にする ・・・・
  62. 62. dvmHeapBitmapSetObjectBit() <ul><li>例えば、Javaプログラムでnew()してオブジェクトを割り当てた場合、ヒープを割り当てる。
  63. 63. このヒープの先頭はオブジェクトデータ。この中のデータ構造を見ると割り当てた領域のサイズはわかるため、先頭だけbitを立てればよいと思われる。(割り当てた領域分のbitを立てる必要はない)
  64. 64. とにかく、 ヒープを割り当てると、 liveBits を立てる、というのが重要 。 </li></ul>
  65. 65. <ul>再び dvmCollectGarbageInternal ( 処理概要 1) </ul><ul><li>dvmCollectGarbageInternalはおおよそ、以下のことを行ってコンカレントGCを行っている。
  66. 66. 1.他スレッドを動けないようにする(suspend)
  67. 67. 2.各オブジェクトのrootに印をつける
  68. 68. 3.card領域のクリア(card領域は後述)
  69. 69. 4.他スレッドが再度動けるように。(resume)
  70. 70. 5.印のついたオブジェクトをたどって印をつける </li></ul>
  71. 71. <ul>再び dvmCollectGarbageInternal ( 処理概要 2) </ul><ul>6.他スレッドを動けないようにする(suspend) 7.再度ルートをチェックして印をつける 8.再度マークのついたオブジェクトからたどって印をつける 9.不要なヒープを解放する。 10.他スレッドが動けるようにする(resume) </ul>
  72. 72. <ul>再び dvmCollectGarbageInternal ( 話の構成 ) </ul><ul><li>これからは、以下の構成で話を進めてみます。
  73. 73. 構成1.ルートに印をつける(1 – 4)
  74. 74. 構成2.印のついたオブジェクトが指している他のヒープに印をつける(5-6)
  75. 75. 構成3.他スレッドが動作することによりメモリの状態が変わる可能性があるので、再度印をつける(7-8)
  76. 76. 構成4.不要なヒープを解放する(9) </li></ul>
  77. 77. <ul>dvmCollectGarbageInternal ( ルートのみに印をつける ) </ul><ul><li>dvmHeapMarkRootSet()をコールして、ルートに印をつける。
  78. 78. 例えば、Javaプログラムでnewされたオブジェクトの場合、ルートに印をつけるためにdvmGcScanRootClassLoader()が呼ばれる。
  79. 79. この中で、グローバル変数gDvmが持っているオブジェクトのルートに対して印をつけています。 </li></ul>
  80. 80. <ul>dvmCollectGarbageInternal ( ルートのみに印をつける ) </ul><ul>markClassObject()はdvmMarkObjectNonNull - markObjectNonNullを呼び出す。 この中では、setAndReturnMarkBit()にmarkbitとobjを渡す。objのアドレスに対応したmarkbitに印をつける(1を立てる) </ul>1 MarkBits ヒープ領域 使われている領域の 先頭に対応した ビットを 1 にする ・・・・
  81. 81. <ul>dvmCollectGarbageInternal ( ルートのみに印をつける ) </ul><ul>dvmCollectGarbageInternal() から dvmClearCardTable() を呼び出し、 CardTable を 0 クリアする。この時点では詳細は割愛しますが、押えてほしいポイントは二つ。 <li>1. ひとつの cardTable はある特定のヒープメモリ 128byte に対応する。
  82. 82. 2. ヒープメモリ上のオブジェクトが保持する他のオブジェクト参照の変更などの事象が発生すると、対応する cardTable は「 Darty 」な状態になる。 </li></ul>
  83. 83. <ul>dvmCollectGarbageInternal ( ルートのみに印をつける ) </ul><ul><li>cardtable と livebit の関係 </li></ul>cardTable ・・・・ liveBits アドレス情報の変更 などでヒープ上の オブジェクトが変更 されると「Darty」 な状態になる ・・・・ livebit16個(ヒープ 128byte)が一つの CardTableに対応する
  84. 84. <ul>dvmCollectGarbageInternal ( 印のついたオブジェクトをたどる ) </ul><ul><li>dvmHeapScanMarkedObjects()を呼び出す。
  85. 85. この関数の中では2つの関数を呼び出している
  86. 86. 1.dvmHeapBitmapScanWalk()
  87. 87. -> 先頭からmarkBitsを順次たどり、印のついたヒープ(オブジェクト)が参照している別のヒープ(オブジェクト)に印をつける
  88. 88. 2.processMarkStack()
  89. 89. -> 印を付けるのを後回しにしたヒープ(オブジェクト)に印をつける
  90. 90. (「後回し」の意味はあとで。) </li></ul>
  91. 91. <ul>dvmCollectGarbageInternal ( 印のついたオブジェクトをたどる ) </ul><ul><li>dvmHeapBitmapScanWalk()
  92. 92. まず最初にmarkBitをたどって印のついた箇所を見つけます。 </li></ul>1 markBits ヒープ領域 LiveBitをたどって 1を見つけた! ・・・・
  93. 93. <ul>dvmCollectGarbageInternal ( 印のついたオブジェクトをたどる ) </ul><ul><li>次にその領域の先頭をみます。例えばJavaオブジェクトの場合は以下のとおりです。 </li></ul>ヒープ領域 ・・・・ ヒープの 別領域 ヒープの 別領域 Javaオブジェクトの 場合、各メンバへの アドレスを保持して いる。
  94. 94. <ul>dvmCollectGarbageInternal ( 印のついたオブジェクトをたどる ) </ul><ul><li>見つけたヒープアドレスが、現在探索対象のヒープアドレス (finger) よりも大きなアドレス値の場合、 markBits の対応ビットに 1 を立てる。 </li></ul>ヒープ領域 ・・・・ 1 markBits 現在検索対象の 次のビットは 「finger」という markBitsの該当 ビットに印 ・・・・ 1 参照
  95. 95. <ul>dvmCollectGarbageInternal ( 印のついたオブジェクトをたどる ) </ul><ul><li>該当領域内で見つけたヒープアドレスが、 finger よりも小さなアドレス値の場合、 markbit のビットに 1 を立てないで、 stack というデータ構造内にアドレスを保存する。 </li></ul>ヒープ領域 ・・・・ markBits finger markBitsの該当 ビットに 印を つけない ・・・・ 1 参照
  96. 96. <ul>dvmCollectGarbageInternal ( 後回しにしたオブジェクトに印 ) </ul><ul><li>Stackに貯めてあるアドレスに対応したmarkBitに印をつけていきます。
  97. 97. 1.stack内のアドレスを一つ取り出す
  98. 98. 2.1で取り出したアドレスに対応したmarkBitsに印をつける
  99. 99. 3.2で印をつけた領域を探し、別のヒープを参照しているアドレスがいたらそれをヒープに詰め込みます。
  100. 100. 1から3をstackの中身がなくなるまで繰り返します。 </li></ul>
  101. 101. <ul>dvmCollectGarbageInternal ( 後回しにしたオブジェクトに印 ) </ul>stack データ構造 ヒープ領域 他のアドレスを 参照しているか 調べる 1 markBits アドレス値 他領域を参照する アドレス値 アドレス値をひとつ取り出す 見つけたアドレス値を突っ込む アドレス値を参照している場合
  102. 102. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>31ページから36ページの処理を行っている間はミューテータが動けるため、ヒープの状態が変わる可能性がある。
  103. 103. そのため、今度はミューテータが動けない状態にした上で再度ヒープを調査して、使用中の領域に再度印をつける。 </li></ul>
  104. 104. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>dvmHeapReMarkRootSet()を呼び出す。
  105. 105. まず最初にルートのみ印をつけ直す。印の付け方は基本的に27ページ、28ページと同様なので省略します。 </li></ul>
  106. 106. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>dvmHeapReScanMarkedObjects()を呼び出し、ルート以外のヒープのうち、変更のあった箇所に印をつけます。
  107. 107. ただし、再度ヒープの先頭からたどっているのでは効率が悪いです。
  108. 108. そこで、dvmHeapReScanMarkedObjects()では一工夫しています。 </li></ul>
  109. 109. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>29ページ、30ページで説明したcardTableを覚えていますか? </li></ul><ul>右の図のように、対応するヒープメモリ128byte上にあるオブジェクトのアドレス情報が変更されるなどすると、cardTableはDartyになります。 </ul>A B C 今までオブジェクトBを参照して いたが、今度はオブジェクトB でなくオブジェクトCを参照 するようになった。
  110. 110. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>以下の図のように変更された可能性の高いヒープ領域を見付だし、該当領域内の全てのオブジェクトに再度印をつけなおします。 </li></ul>・・・・・・ ・・・ cardTable 変更された 領域発見! ・・・ オブジェクト をサーチ markbits
  111. 111. <ul>dvmCollectGarbageInternal ( 再度印をつける ) </ul><ul><li>こうすることで、全てのmarkbitをたどらなくてもよくなるため、変更された可能性のあるオブジェクトをより少ない手間で絞り込めます。 </li></ul>
  112. 112. <ul>dvmCollectGarbageInternal ( 不要なヒープを解放する ) </ul><ul><li>これでいよいよ不要なヒープを解放する準備ができました。
  113. 113. dvmHeapSweepUnmarkedObjects()を呼び出し、不要なヒープの解放を行います。
  114. 114. 解放のメインとなるのはdvmHeapBitmapSweepWalk()です。 </li></ul>
  115. 115. <ul>dvmCollectGarbageInternal ( 不要なヒープを解放する ) </ul><ul><li>ここでもう一度復習。 </li></ul>・・・ markbits ・・・ livebits 割り当て済みメモリ の先頭アドレスにあたる ビットは1 GCの時点で使われている メモリ領域は1
  116. 116. <ul>dvmCollectGarbageInternal ( 不要なヒープを解放する ) </ul><ul><li>この二つのビットマップを使って「割り当てされたけど、今は使われていない」ヒープ位置を割り出します。そのために以下のようなビット演算を行います。
  117. 117. live & ~mark </li></ul>live ~mark & 意味 0 0 0 初めから割り当てられていない もしくは割り当てられた領域の途中 0 1 0 これはありえない 1 0 0 割り当てられて、今も使われている 1 1 1 割り当てられたけど、今は使われていない
  118. 118. <ul>dvmCollectGarbageInternal ( 不要なヒープを解放する ) </ul><ul><li>46ページで新たに作成したビットマップで1が立っている箇所に対応したアドレスを求めて(DECODE_BITSマクロ)、ビットが立っている箇所に対して解放処理を呼び出す。
  119. 119. 具体的には(FLUSH_POINTERBUFマクロ経由でsweepBitmapCallbackが呼ばれる。
  120. 120. これによってめでたくメモリが解放されます。 </li></ul>
  121. 121. <ul>まとめ </ul><ul><li>DalvikのGCはMark & Sweepでした。
  122. 122. コンカレントGCは、処理の途中でミューテータが動けるようにすることでミューテータの処理が長い時間中断される事を防ぎます。
  123. 123. 2度目のMarkフェーズでは一工夫することで再度全ヒープをたどらない工夫をしています。
  124. 124. sweepのために二つのビットマップを上手に使っています。
  125. 125. これ以上知りたければ、ソース読んでみてください。勉強になります。 </li></ul>
  126. 126. お礼 <ul><li>初めての勉強会発表にも関わらず、ご清聴ありがとうございました。 </li></ul>

×