staticフィールドは、意図せず初期化されることがある
概要
- AndroidをJavaで書くマンしている
- 今まで、staticに突っ込んだ値は、アプリのプロセスが終わるまでは生きていると思っていた
- プロセスが終わるまで生きてるんだから、staticに値つっこんで管理せばええやろ〜〜〜と思って、そういったコードを書いている箇所があった
- 今回の場合、staticフィールドに状態を持たせるようなコードを書いていた
- 「staticの値って、ホントにプロセス終わるまで生きてるの?」「その理解、ホント?」とレビューで突っ込まれた
- 先に書いていた理解してた事柄は、学生の時に「そうなんだ〜。ふ〜ん。」程度に覚えていた知識で自信もなかったので、調べたのでそのメモ
先に結論
- staticも初期化される可能性は普通にある
- staticとするのは定数のみにして、状態管理に使わないほうが良さそう
(この理解に間違いがあれば、何かしらでツッコミを入れてもらえると、とてもありがたいです)
Staticな変数ってそもそもなに
インスタンスではなく、クラスに紐付けられた変数
インスタンス変数 ↔ クラス変数
JVMの概要
じゃ、そのクラスの情報はどう管理されているか。 JVMがどうなっているか調べてた。
JVM | Java Virtual Machine - Javatpoint
ざっくり
- JVMが起動すると、
ClassLoader
なるものが動作し、アプリケーションで利用するクラスを読み込む - 読み込んだ内容は、メモリに展開され、画像中央の白枠のようにそれぞれ領域別に展開される
クラスローダーとかいうやつが、鍵っぽい
クラスローダー
JVM起動時にアプリケーションで利用するクラスを読み込んで、メモリに展開するくん
クラスローダにはいくつか種類がある
ブートストラップ・クラス・ローダー
- 起動時にまず呼び出されるクラスローダ
- JVM自体の実行に必要となるシステム・クラスをロードするのが役割
- 注意として、読み込めるすべてのクラスをロードするのではなく、アプリケーションからのクラス参照があった時にロードされる
- JDKディストリビューションによって提供されているすべてのクラスがこのクラス・ローダーによってロードされると理解して問題ないとのこと
- オプション
-Xbootclasspath
をいじると、ブートストラップ・クラス・ローダーがロードできるクラス・セットの範囲を変更できる
拡張クラス・ローダー
システム・クラス・ローダー
- アプリケーション・クラスおよびクラスパス上にあるクラスをロードする
- 拡張クラス・ローダーと同様に、最初に親に委譲し、その後必要なら、そこではじめて対象のクラスを検索し、解決する
- オプション
-cp
でクラスパスを指定できる
ロードについて
大きく4つのフローがある
static変数に値を突っ込むのは、準備
の段階らしい
- 検証
- クラスが破損していないこと、および構造的に正しいことを検証する
- 実行時コンスタント・プールが妥当
- Heapにある定数やシンボルを保存する領域
- コンスタントプール - Wikipedia
- 変数の型が正しいこと
- 変数がアクセスされる前に初期化されている
- 準備
- 静的フィールドをそれぞれの型に合うデフォルト値に初期化する処理が含まれる
- 例:準備後はint型のフィールドには0、参照はnullになる
- 解決
- 実行時コンスタント・プール内のシンボリック参照が、実際に必要とされる型の妥当なクラスを指し示していることをチェックする
- シンボリック参照の解決が契機となり、参照先のクラスのロードが行われる
- 初期化子群の実行
- クラスが準備済み、検証済みであることを前提として、クラスの初期化子(イニシャライザ)を実行する
- イニシャライザについては以下がわかりやすかった。
- インスタンス初期化子の存在意義が理解できました | jikkenjo.net
- すべての静的初期化ブロックのコードを結合した静的初期化子(スタティック・イニシャライザ)メソッドも実行される
- 初期化プロセスは、ロードされたクラスごとに1回のみ実行すべきものであり、同期的に処理される
- これは特に、クラスの初期化によって他のクラスの初期化が起動される恐れがあるため、デッドロックに注意して初期化を行う必要があるから
- 初期化処理が重複して行われないようにしたいから、という理解でいいんだろかな
- クラスが準備済み、検証済みであることを前提として、クラスの初期化子(イニシャライザ)を実行する
アンロードについて
ロードはJVMの起動時に行われ、不要となったクラスのアンロードは、そのクラスが利用されなくなったときに行われる
これが実行されるとクラスのデータも破棄されるので、staticフィールド
も破棄される
どういったときにアンロードされるのかは、以下のとおりらしい。
クラスのアンロードは、そのクラスが不要になった時に起こる。 クラスが不要になる条件は、以下の 3 つの条件を全て満たす必要がある。
- ヒープ中からそのクラスのインスタンスがなくなること。
- そのクラスの static メソッドを実行中のスレッドがいないこと。
- そのクラスをロードしたクラスローダーを現わす ClassLoader 派生型のインスタンスがヒープ中からなくなること。
クラスが 1. ~ 3. の条件を満たしているかどうかの判断は、実装的な理由により GC 時に行われる Java VM が多い。
アンロードが起こるとstaticフィールドの値も吹き飛ぶことがわかった。
でもそれ、AndroidのJVMだと、どれぐらいの頻度でおこるもんなの...?
公式には以下のように書いてあった
先の1~3と同じ条件と思っていれば良さそうな気がした
The class references, field IDs, and method IDs are guaranteed valid until the class is unloaded. Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android. Note however that the jclass is a class reference and must be protected with a call to NewGlobalRef (see the next section).
ただ、DalvikとARTどっちもそういう理解でいいのか...?
Dalvikについては、以下のStack OverflowにDalvikのエンジニアやってた人がコメントしている
先の1~3と同じ条件と思っていれば良さそうな気がした
ARTについては、それらしい文献を見つけれていない
明示的にARTって書いていないけど、公式にアンロードされることがあるって書いてあるし、そう思っておくことにする
また、以下の記事があった
公式ドキュメントでは「あんまりアンロード起こらんよ」とはいいつつ、わりと普通にunloadされているらしい
ご存知の通り、Androidはメモリが逼迫するとバックグラウンドにあるActivityやらServiceやらを殺していく。 殺されるのはそういったAndroid特有のオブジェクトだけではなく、 アプリのライフサイクルとは無縁の上記ContextHolderのようなオブジェクトも例外では無い…ように感じる。 というのも、実際に上記のようにstaticで参照を保持しているフィールドにアクセスするアプリで、 ぬるぽでクラッシュしたレポートがコンスタントに上がってきているので。 クラスがUnloadされると、次回必要になった際に再Loadされる。 その時に初期値を設定していないstaticフィールドはnullで初期化される。 そこで本来はApplicationクラスのonCreate()でContextを設定したいところが、 プロセスまでは殺されていないためにApplicationクラスのonCreate()が再度呼ばれず、 staticフィールドはnullのままになってしまうのである。
結論
- staticも初期化される可能性は普通にある
- staticとするのは定数のみにして、状態管理に使わないほうが良さそう
(この理解に間違いがあれば、何かしらでツッコミを入れてもらえると、とてもありがたいです)