84
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】Swiftのメモリ管理を内部の仕組みから学ぶ

Last updated at Posted at 2019-10-12

普段コードを書いている時に
Swiftが内部で
どのようにオブジェクトを管理しているのかについて
考えることはあまり多くないかもしれません。

しかし
非同期処理を扱う場合など

DispatchQueue.main.async { [weak self] in
    ...
}

などのように
weakといった
キーワードを使用することは多くあると思います。

これは「弱参照」と呼ばれ
直接の参照(強参照)を持たないように
Swiftの内部に指示を出して
循環参照を起こさないための仕組みです。

こういった適切なメモリ管理を行わないと
メモリが解放されないことでランダムにクラッシュするなど
原因がわかりづらい不具合を発生させる可能性があります。

そこで今回は
weakなどを使用することで
Swiftの内部で何が起こるのかを見ていき

weakunownedの使い方や
オブジェクトのライフサイクルについて
学んでみたいと思います。

メモリの3つ仮想的な領域

メモリ自体はただのバイトの配列ですが
プログラミングという観点で考えると
主に3つの領域に分かれます。

  • スタック領域 全てのローカル変数を保管している場所
  • グローバルデータ 静的な変数や定数や型のメタ情報を保管している場所
  • ヒープ領域 オブジェクト※を保管している場所
           

    実行時にメモリを割り当てられ
    ある時点で解放される「寿命」を持つものを指します。
    Swiftでは主に参照型(reference type)を意味します。

ARC(Automatic Reference Counting)

メモリ管理には「オーナーシップ(所有権の帰属)」という概念が大切です。
これは、あるオブジェクトを誰が解放する責務を持っているかということを意味します。

詳細に関しては下記のOwnershipManifestに記載されています。
https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.md

このオーナーシップを管理する仕組みとして
SwiftはARCを使用しています。

ARCはオブジェクトの参照されている数(参照カウント)を保持しておき
カウントがゼロになるとメモリから自動で解放される仕組みです。

2つのレベルの参照(強参照と弱参照)

Swiftでは参照に2つのレベルがあります。
それが冒頭でも登場した
強参照(strong reference)

弱参照(weak reference)
です。

さらに
弱参照の派生として**非参照(unowned reference)**があります。

強参照(strong reference)

Swiftでは
強参照がある限り
オブジェクトは生存し続けることができ
逆になくなるとメモリから解放されます。

SwiftではJavaのガベージコレクションのように
自動でメモリを解放する仕組みは持っていないため
循環参照を引き起こす可能性があります。

たとえば
オブジェクトAとオブジェクトBが
お互いにオブジェクトへの参照を持っている状態です。

これを防ぐためにはweakなどの記述が必要になってきます。

弱参照(weak reference)

弱参照を使用することで
循環参照を断ち切ることができますが
弱参照を持っていたとしても
強参照がなくなればオブジェクトはメモリから解放されてしまいます。

そして
参照しているオブジェクトを使おうとしてもnilになっています。

そのためweakを付けたオブジェクトはOptionalとして扱います。

非参照(unowned reference)

弱参照とほぼ同じですが
参照しているオブジェクトがnilの状態で使おうとすると
assertionエラーでプログラムはクラッシュします。

そのため、unownedは、参照がなくならないと想定しているのに
予期せず参照が解放されてしまっている不具合を発見するのに役立ちます。

unownedを使用する基準としては
参照するオブジェクトと参照されるオブジェクトの寿命が同じような時です。

たとえばクラスの中でlazyを付けた変数を定義したとします。


class ViewController: UIViewController {
   lazy var label: UILabel = { [unowned self] in
      self.someSetup()
      ...
   }() 
}

これはViewControllerクラスの変数label
self(ここではViewController)を参照しています。

これはクラスオブジェクトが解放されるタイミングで
label変数も解放されるので
selfの参照がなくなることはありません。

参照については下記のドキュメントに詳細が記載されています。
https://github.com/swiftlang/swift/blob/main/docs/WeakReferences.md

Swift Runtime

ARCはSwift Runtimeというライブラリで実装されています。

他にもSwift Runtimeでは
実行時にジェネリクスやプロトコルを具体的な型に解決する
などの重要な機能を有しています。

全てのオブジェクトは
**HeapObject**というstruct
で表現されます。

HeapObjectはオブジェクトの型のメタ情報と参照カウント(RefCount)を持っています。
RefCountにはstrongweakunowned用の3種類があります。
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h

SwiftのコンパイラはSIL生成段階(SIL Generation)※で
swift_retain()
swift_release()
というメソッドを適切な場所に差込みます。

これによってHeapObjectの作成や解放がされます。


SIL生成段階(SIL Generation)はSwiftのコンパイル時の一つのフェーズです。
下記のドキュメントに詳細が記載されています。
https://swift.org/compiler-stdlib/#compiler-architecture

Side Table

全てのオブジェクトは
弱参照(weak reference)用のRefCountを持てるものの
実際には多くのオブジェクトで弱参照を使いません。

そこで、弱参照用のRefCountにメモリを割り当てても無駄になることが多いため
弱参照の情報はSide Table※という別の場所に保管され
本当に必要になった時にメモリが割り当てられるようになっています。


正式にはHeapObjectSideTableEntryです。
内部ではオブジェクトのポインタとRefCountを持っています。
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h#L1257

弱参照が示すメモリアドレスは参照したいオブジェクトではなく
このSide Tableを示しています。

こうすることで
無駄にメモリを消費することがないことに加え、
直接オブジェクトを参照していないため
オブジェクトの解放と弱参照の参照のタイミングが競合することなく
弱参照を取り除くことができます。

オブジェクトのライフサイクル

下記のコメントを参考にすると
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h#L112

Swiftのオブジェクトは3つの参照の保持の仕方によって
状態を5つに分けることができます。

  • LIVE
  • DEINITING
  • DEINITED
  • FREED
  • DEAD

簡単に図にすると下記のように状態が変化していきます。

名称未設定ファイル (2).png

次にそれぞれの状態について
見ていきます。

各状態で弱参照(Side Table)があるかないかで
挙動や次に遷移する状態が変化していきます。

LIVE without side table

オブジェクト

生存している

参照カウント

強参照1 非参照1 弱参照1で初期化される。

Side Tableと弱参照カウントのメモリ割り当て

なし

強参照変数の操作

正常に機能する

非参照変数の操作

正常に機能する

弱参照変数を使用した時の挙動

起こり得ない

弱参照変数へのオブジェクトの代入

Side Tableを追加する
LIVE with Side Table状態になる

次の状態への遷移

参照がゼロになった時
deinitが呼ばれDEINITINGの状態になる

LIVE with side table

弱参照変数の操作

正常に機能する

それ以外は
LIVE without side tableと同じ

DEINITING without side table

オブジェクト

deinit()を実行中。

強参照変数の操作

何も起こらない。

非参照変数を使用した時の挙動

swift_abortRetainUnowned()で処理が中断されます。
https://github.com/apple/swift/blob/ebcbaca9681816b9ebaa7ba31ef97729e707db93/include/swift/Runtime/Debug.h#L122

非参照変数へオブジェクトを代入した時の挙動

正常に機能する

弱参照変数を使用した時の挙動

起こり得ない

弱参照変数へオブジェクトを代入した時の挙動

nilが代入される

次の状態への遷移

参照がゼロになった時の挙動
deinit()が完了し
swift_deallocObject()というメソッドが呼ばれる。

canBeFreedNow()で弱参照または非参照があるかどうかをチェックする。
canBeFreedNowがtrueの場合
オブジェクトは解放されてDEINITEDの状態になる。

DEINITING with side table

弱参照変数を使用した時の挙動

nilを返却する

弱参照変数へオブジェクトを代入した時の挙動

nilが代入される

canBeFreedNow()は常にfalseになり
そのままDEAD状態にはならない。

その他はDEINITING without side tableと同じ。

DEINITED without side table

オブジェクト

deinit()は完了しているが
非参照は存在している。

強参照変数の操作

起こり得ない

非参照変数を使用した時の挙動

swift_abortRetainUnowned()の中でロードを停止している

非参照変数へオブジェクトを代入した時の挙動

起こり得ない

弱参照変数の操作

起こり得ない。

次の状態への遷移

非参照カウントがゼロになった時
オブジェクトは解放されてDEAD状態になる

DEINITED with side table

弱参照変数を使用した時の挙動

nilが返却される

弱参照変数へオブジェクトを代入した時の挙動

起こり得ない

次の状態への遷移

非参照カウントがゼロになった時
オブジェクトは解放されて弱参照カウントが減り
オブジェクトはFREED状態になる

他はDEINITED without side tableと同じ

FREED without side table

起こり得ない

FREED with side table

オブジェクト

オブジェクトは解放されているが
弱参照がSide Tableに残っている

強参照の操作

起こり得ない

非参照の操作

起こり得ない

弱参照変数を使用した時の挙動

nilが返却される

弱参照変数へオブジェクトを代入した時の挙動

起こり得ない

次の状態への遷移

弱参照カウントがゼロになった時
Side Tableオブジェクトは解放され
オブジェクトはDEAD状態になる

DEAD

オブジェクトもSide Tableもなくなっている

まとめ

Swiftの内部の仕組みから
メモリ管理について見ていきました。

普段はあまり意識していませんでしたが
こういう知識を知っていることで
原因がわからない不具合などの解決にも
役に立つことがあるかもしれません。

また
XcodeにはMemoryDebuggerもあり
そのグラフを理解するのに役に立つかもしれません💡

もし何か間違いなどございましたら
ご指摘頂けましたらうれしいです🙇🏻‍♂️

参考記事

84
62
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
84
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?