メモリ管理方法
SwiftはARC(Automatic Reference Counting / 自動参照カウント)というメモリの管理方式を採用しています。
一方で、JavaやKotlin・C#といった言語はメモリ管理の方式としてGC(Garbage Collection / ガベージコレクション) 1 を採用してます。
この記事では、ARCの特徴(光)とそれにまつわる問題点(影)を、GC方式と比較したりしながら取り上げます。
なお、長いので3行でまとめて欲しいという方は、まとめを読んでいただければと思います。
ARCとは
ARCは参照カウントという手法でメモリを管理します。この参照カウントの仕組みをざっくり言うと、参照型のインスタンス(オブジェクト)が各々どれだけ他から参照されているか(=変数などに保持されているか)を常にカウントしておき、どこからも参照されなくなったら(=カウントが0になったら)、そのインスタンスをメモリ上から解放するというシンプルな仕組みです。
ARCでは、カウントを増減させる部分の処理をコンパイラが自動的にコードに埋め込みます。つまり、プログラマが手動でカウント処理を指示しなくても、自動で参照がカウントされるのでARC(自動参照カウント)という名称になっています。
GCとは
GCには様々な種類がありますがこちらもざっくりと言うと、メモリ上にあるオブジェクト全部の中から実行中のプログラムから参照されていない不要なオブジェクトを探し出しそれを解放するという仕組みです。
ただ、この解放処理はまともに行うとかなり重たい処理で、実行中のアプリの処理も中断2されてしまいます。それで、解放処理を行うタイミングを工夫したり、管理するメモリ領域を分割したりするなど、なるべく効率良く処理ができるように様々な改良がなされています。
いずれにしても、プログラマ側ではメモリ管理に関しては特に実装することはなく、GCは自動で行われるようになっています。
ARCの特徴と問題点
参照されなくなったらメモリが解放される
ARCではそのインスタンスがどこからも参照されなくなったら(=不要になったら)、その直後にメモリからインスタンスが解放3されます。
GCの方は解放タイミングが自動で判断されるので、オブジェクトが不要になってもすぐに解放されるとは限らず、実際にメモリから解放されるタイミングは不明です。
メモリのピーク使用量が抑えられる
これはGCと比較した場合の話ですが、特にモバイルアプリにおいてメモリのピーク使用量が抑えられメモリ枯渇が起こりにくくなります。
一般的なモバイルアプリの特徴はインタラクティブなアプリである、つまり、ユーザの操作がトリガーとなってAPIを実行したりUIを更新したりといった種々の処理が実行されます。これをメモリの使用量という観点から見ると、ユーザの操作後にメモリの使用量が跳ね上がり、そこから処理が完了するにつれてメモリが解放されて減っていくといった形の波が生じます。
もう一つの特徴として、モバイルアプリは(最近は高スペックな端末が増えたとはいえ)利用できるメモリ量が限られているという点があります。それでメモリのピーク使用量が大きいとメモリが枯渇してしまい、動作が遅くなったりアプリが強制終了してしまうといったことが起こります。
GCの場合は、実際の解放処理が行われるまでの間、不要になったオブジェクトもメモリに残ったままです。その為、メモリのピーク使用量 = 解放待ちのオブジェクトの分 + 実際の使用量となり、メモリの使用量が跳ね上がった時には一時的にメモリが枯渇することがあります。また、そこまでいかなくても一気に重たいメモリ解放処理が走るので、GCスパイクと呼ばれるプチフリーズが起こることもあります。その点ARCは不要になったメモリがその都度こまめに解放されるので、メモリのピーク使用量と実際に使っているメモリ量はほぼ同じ状態となり、メモリ不足が起こりにくくなっています。
とはいえ、ARCにはオーバーヘッドが発生するという問題点があります。変数にインスタンスを代入したり、メソッドの引数として渡したり、その変数がスコープを抜けたりする度に参照カウンタのチェック処理が入るからです。それ自体はあまりコストのかからない処理とはいえ、ループ処理のように回数が多くなればそれなりのコストとなるので、アプリによっては問題となることがあります。
また、そもそもサーバサイドで使う場合のように潤沢なメモリが準備されている環境や、メモリ使用量がほぼ一定になる処理の場合には、メリットよりオーバーヘッドのデメリットの方が際立ってきます。なお、Swift5でOwnership(所有権)が導入される理由の一つには、このオーバーヘッド問題を解決するという目的があります。
メモリ解放のタイミングの予測や制御が可能
ARCではインスタンスが保持されているかどうかでメモリから解放されるかどうかが決まるので、大抵の場合、プログラマはいつメモリが解放されるのかを予測し制御することができます。
これは普段から役立つといったものではないですが、メモリ周りのパフォーマンスチューニングが必要になった時に有るのと無いのでは大きな違いとなります。GCの場合は事前にちゃんと不要なオブジェクトを破棄しておいても、実際のメモリ解放のタイミングが制御できない為、肝心な場面でGCスパイクが発生してしまう4ことがあります。
またARCの場合は、あるオブジェクトが解放されていないと思える時にdeinit
が呼ばれるかどうかで手軽に確認できるのも便利です。
循環参照によるメモリリークが起こる
これまでの2つの特徴はまさにARCの「光」、良い面でしたが、それを実現する為に犠牲となっているいわば「影」の部分がこの循環参照によるメモリリークの問題です。
参照型のインスタンス同士がお互いを保持し合っていると、いつまで経っても参照カウントが0にならないのでメモリが解放されずメモリリークを起こしてしまいます。GCの場合は、循環参照になっていても丸ごと解放される為、メモリリークは発生しません。
しかも、この循環参照を正しく解消するのはプログラマの責任となります。struct
のような値型を使ったりweak
を使ったりすれば解消できるとはいえ、特にクロージャが関係する場合はweak
とunowned
のどちらを使うべきなのか、そもそも循環参照にならないパターンなのかなどを切り分けて判断するのは難しい問題です。また、ケアレスミスで循環参照が入り込んでしまうリスクもあります。
ただ、これがものすごくデメリットなのかといえば一概にそうとは言えないと思います。最近よく同じ処理をSwiftとKotlin両方で書く機会が多くて気づいたのですが、Swiftの場合はキャプチャリストを見るだけで、クロージャ内の処理が呼び出し元のオブジェクトにどういう影響を与えるのかが判るというメリットがあります。確かにKotlinのあまり難しく考えずにコーディングを進められる生産性やメモリリークの心配をしなくて良いという安心感は大きいですが、やたらと非同期処理が多くなりがちなモバイルアプリを保守していくという場合にはSwiftの方が合っているかもしれません。
まとめ
ARCはモバイルアプリとの相性が良く、パフォーマンスへの影響も少なめです。シンプルな仕組みのゆえに、必要な場合にはプログラマ側で制御しやすいですが、その反面、循環参照によるメモリリークという注意すべき点もあります。
-
本来はARCを含む参照カウント方式もGCの一種ですが、本記事では便宜上、GCという語は参照カウント方式以外のGCを指して用います。 ↩
-
"Stop the World"と呼ばれる現象です。DIOのスタンドみたいな名前ですが、実際に遭遇するとDIOと同じくらいの強敵です。 ↩
-
iOSのようにautoreleasepoolが使われている場合は、そのブロックを抜けたタイミング(通常はイベントループを抜けた時)に解放されます。 ↩
-
最近だとUnity界隈でこの阿鼻叫喚が見られていましたが、その改善の為にUnity 2019からインクリメンタルGCが導入されました。ただ、GCが複雑な仕組みを持っていると、うまくいく場合は良いのですが、より予測が困難となりトラブルが起こった時の解決が難しくなってしまうこともあります。 ↩