参照カウント

出典: フリー百科事典『ウィキペディア(Wikipedia)』

出典: フリー百科事典『ウィキペディア(Wikipedia)』

参照カウント(さんしょうカウント、: reference counting)は、メモリオブジェクトのライフサイクル(寿命)管理に使用される方式のひとつ。ガベージコレクションの実装方法およびガベージコレクタの動作方法のひとつとしても利用される。また、コピーオンライトの実装方法としても多用される。

処理の概要[編集]

  • すべてのオブジェクト(メモリ上に置かれているデータの単位)に対して、参照カウントと呼ばれる整数値を付加しておく。これは、このオブジェクトへの参照(あるいはポインタ)がシステム全体にいくつ存在しているかを数えるものである。
  • オブジェクトへの参照が変化するたびにこの値は随時書き換わる。
  • 参照カウントが0になったものについては破棄が許される。

共有された単一のオブジェクトへの参照ではなく、独立したデータを擬似的に表現する場合は、下記の処理を追加する。

  • オブジェクトのコピーが要求されても、実際にはコピーを行わず元のオブジェクトへの参照を返し、参照カウントに1加える。
  • オブジェクトの変更が行われる場合は、以下の手順で行う
    • 参照カウントが1であればそのまま書き換える。
    • 参照カウントが2以上であれば、元のオブジェクトをコピーして参照カウントが1の新オブジェクトを作成し、それを書き換える。元のオブジェクトの参照カウントは1減らす。

特徴[編集]

長所[編集]

  • 処理が単純であり、高速である。
  • オブジェクトを多数生成し、すぐに参照を切るような処理においても、参照がなくなったことがその場で検知され、迅速に破棄が起きる。利用できるメモリが少ない状況では大きな利点となる。
  • ガベージコレクタのためのスレッドやプロセスを必要としない。
  • ガベージコレクションとコピーオンライトを同時に実装できる。

また、システムの空き時間(アイドル時)に動作する方式のガベージコレクションとは異なり、参照カウント方式は通例決定論的 (deterministic) に動作するため、オブジェクトの解放タイミングを確実に制御したい場合に有利である。

短所[編集]

  • オブジェクト同士が互いに参照しあうなど、参照が循環している場合、参照カウントが0にならないためにオブジェクトが破棄されないという問題がある。(詳しくは後述)
  • 単純な実装では大量のオブジェクトが一斉に解放されることがあり、CPUの空き時間を利用してガベージコレクションを行う方法と比べると、処理が遅くなる場合もある。
    • コンテナオブジェクトなど、解放されるオブジェクトが、多くのオブジェクトを参照している場合に起こりやすい。
  • 対象オブジェクトが小さく、コピーが頻繁に行われるような場合は、参照カウントの空間的・時間的オーバーヘッドが問題となる場合がある。

用途[編集]

文字列オブジェクトの実装手段としては適しており、多くのシステムで採用されている。 これは、文字列であれば内部で他のオブジェクトを参照しないので、短所の多くには影響がないためである。

単純なビット列などのデータも同様に適している。

循環参照の問題点[編集]

参照カウントには、循環参照によりデータを解放できなくなる(メモリリークが発生する)という問題がある。

例えばC++向けのBoost C++ライブラリあるいはC++11規格以降の標準C++ライブラリには、参照カウントベースのスマートポインタを実現するクラステンプレートとして、それぞれboost::shared_ptrあるいはstd::shared_ptrが用意されているが、これを使って自己参照あるいは相互参照を持つクラスを定義してしまうと、参照カウントが適切に減らされずに互いのインスタンスが解放されなくなってしまう。

#include <iostream>
#include <memory>

class A {
public:
    std::shared_ptr<class B> m_refB;
    A() {}
    ~A() {
        std::cout << "Destructor of A is called." << std::endl;
    }
};

class B {
public:
    std::shared_ptr<class A> m_refA;
    B() {}
    ~B() {
        std::cout << "Destructor of B is called." << std::endl;
    }
};

void doTest() {
    std::shared_ptr<A> refA(new A());
    std::shared_ptr<B> refB(new B());
    refA->m_refB = refB;
    refB->m_refA = refA;
} // ここで A および B のデストラクタが呼ばれなくなり、メモリリークする。

int main() {
    doTest();
    return 0;
}

循環参照によるメモリリークを回避するためには、利用を終えたタイミングで明示的に参照を解除するコードを記述するなどの方法があるが、ソースコードが煩雑になってしまい、スマートポインタの利点を打ち消してしまう。

この問題を解消するために、多くのプログラミング言語やソフトウェアライブラリあるいはシステムで弱い参照 (ウィークリファレンス、: week reference) が導入されている。弱い参照とは、参照カウントを増加させない参照である。例えばC++ではboost::weak_ptrあるいはstd::weak_ptrなどが該当する。

一方、マーク・アンド・スイープやコピーGCによるガベージコレクションでは、少なくとも循環参照によるメモリリークの問題は発生しない。

なお、Java.NET Framework (.NET Compact FrameworkMonoも含む) では、いずれも参照カウントベースではないガベージコレクション方式を採用しており[1][2][3][4]、循環参照によるメモリリークは発生しない。例えば以下のJavaコードは合法であり、循環参照していたとしてもガベージコレクションの回収対象となる。

class A {
    public B b;
}

class B {
    public A a;
}

public class Test {
    public static void doTest() {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;
    } // 十分な時間が経過すれば、いずれ GC により回収される。

    public static void main(String[] args) {
        doTest();
    }
}

ただし、非意図的オブジェクト保持(unintentional object retention)が引き起こすメモリリークを回避するために、通常の参照(強参照)の代わりにjava.lang.ref.WeakReferenceのような弱参照クラスのインスタンスを使うこともある[5]

ウィキペディアでの例[編集]

ウィキペディアの「孤立した記事」は、参照カウントが0のものを表示しているだけなので、孤立した記事だけから参照されている記事は孤立した記事と見なされていない(これも循環参照の例である)。

実用例[編集]

  • マイクロソフトComponent Object Model (COM) におけるCOMオブジェクトは参照カウント方式で管理される。
  • プログラミング言語Objective-Cのオブジェクトは参照カウント方式で管理される。
  • プログラミング言語Perlのガベージコレクタは参照カウント方式を用いている。
  • プログラミング言語Pythonのガベージコレクタは主に参照カウント方式を用いている。
ただし、補助的に(伝統的なマーク&スイープとは逆順の探索アルゴリズムによる)世代別GCを併用している[6][7]
  • プログラミング言語C++の規格C++11以降には、参照カウントでオブジェクトを管理するためのクラステンプレートとしてstd::shared_ptrが存在する。
  • Boost C++ライブラリboost::shared_ptrおよびboost::shared_array
参照カウントの増減処理をカスタマイズできるboost::intrusive_ptrもある。

脚注[編集]

関連項目[編集]