宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。
以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
最初に要点だけまとめておくと、
という話です。
JavaScriptでは変数は動的に定義する物なので、可変長の配列なんかも全然何も考えずに使えます。必要なメモリは実行時に勝手に確保されて、使われなくなったらガーベジコレクタがそれを勝手に回収(解放)してくれる、というモデルです。
それに対して、C言語では配列は必ず長さが決まってないといけなくて、可変長の配列のように実行時にデータの量が決まるような変数を使いたい場合は、malloc()
でメモリを確保して使い終わったらfree()
で解放するという事をしないといけません。
js-ctypesはJavaScriptからCのライブラリを呼ぶ物なのですが、ではメモリの確保とかの話はJavaScript側とC側のどっち寄りなのか? というと、これはJavaScript寄りになります。Cのライブラリに渡すために定義した変数のために必要なメモリ領域はその場で自動的に確保されて、使い終わった変数のメモリ領域は勝手に解放されるようになってます。
……ということになってるんですが、実際には期待した通りにメモリが解放されてくれない事があるようです。1つ前のエントリのケースがまさにそうだったのですが、動的に構造体を定義してそのインスタンスを作って……ということをやっていると、関数が呼ばれるごとに消費メモリが増えていって、メモリリークっぽい状態になってしまいました。
以下が問題の箇所です。
var selfUsage = 0;
var pagesCount = self.NumberOfEntries;
if (is64bit) pagesCount = pagesCount * 2;
var PSAPI_WORKING_SET_INFORMATION = new ctypes.StructType
('PSAPI_WORKING_SET_INFORMATION', [
{ NumberOfEntries : ULONG_PTR },
{ WorkingSetInfo : ctypes.ArrayType(ctypes.uint32_t, pagesCount) }
]);
// ここでメモリが確保されてる
var actualSelf = new PSAPI_WORKING_SET_INFORMATION();
if (QueryWorkingSet(GetCurrentProcess(),
actualSelf.address(),
PSAPI_WORKING_SET_INFORMATION.size) != 0) {
let pages = actualSelf.WorkingSetInfo;
let step = is64bit ? 2 : 1 ;
for (let i = is64bit ? 1 : 0; i < pagesCount; i += step) {
if (!(pages[i] & 0x100)) {
selfUsage++;
}
}
selfUsage = selfUsage * systemInfo.dwPageSize;
}
// ここで参照を消してるのにGCされない
actualSelf = undefined;
PSAPI_WORKING_SET_INFORMATION = undefined;
自動でガーベジコレクトされてくれないということは、どっかに参照が残ってしまっててるのか? と思ってコードを見直してみてもそういう事は全然無いし、第一Components.utils.forceGC()
で強制的にGCしてみたらちゃんと消費メモリが減ります(つまり、本当の意味でのメモリリークではないということです。「メモリリークっぽい」と書いたのはそのためです)。
また、GCは一般に重たい処理と言われているので、消費してるメモリの量が一定以上にならない限りは自動ではGCされないのかな? とも思ったのですが、ほっとくとFirefoxの消費メモリが1.8GBとかあり得ないことになったりもしてました。これはどう考えても、そろそろGCするべきだろという時機を逸してしてしまってるとしか思えません。
どうも、js-ctypesとJavaScriptの自動GCは相性が悪いようで、js-ctypesを使ってる時は期待されるようなタイミングで自動的にGCが走ってくれないという事が起こるようです。
この問題に対する対策の方法は2つあります。
Components.utils.forceGC()
で強制的に頻繁にGCさせる。前述した通り、Components.utils.forceGC()
を呼ぶとその時点での消費メモリ量やCPUの忙しさとかに関係無く、強制的にGCを走らせる事ができます。実際に、getMemory()
を実行した時に一緒にComponents.utils.forceGC()
も実行するようにすると、メモリ消費がぐんぐん増えていくというメモリリーク様の症状はぱたりと発生しなくなりました。
が、これは弊害が大きそうなのでできれば避けたい所です。これも前述した通りですが、GCは結構重い処理だそうなので、GCが走る瞬間は動画の再生が止まるなどの問題が起こりる事があります。システムの状態を表示するだけのアドオンなのに、それを入れてると1秒ごとに動画の再生が止まってしまうとか、ちょっとあり得ないです。
GCに期待しないで自分でメモリを管理するというのは、
new
する。new
した構造体への参照を削除する。後のことはGCに任せる。というのをやめて、
malloc()
を呼ぶ。malloc()
が呼ばれ、必要なメモリ領域が確保される。この時、malloc()
で確保されたメモリ領域そのものはGCの管理対象にならない。malloc()
の戻り値であるポインタをjs-ctypes経由でJavaScriptに返すために必要なメモリ領域がCの側で自動的に確保され、その領域がJavaScriptのGCの管理対象になる。free()
を呼ぶ。2で確保されたメモリ領域が開放される。という風にするということです。GCが走らなければこれでもjs-ctypesのポインタ型の変数のために確保されたメモリ領域の分だけ消費メモリは増えていってしまいますが、消費されるメモリの量は桁違いに少ないと考えられますので、こっちの方がマシに自分には思えました。
js-ctypesは、ライブラリを開いて関数を定義してという手順で使う関係上、.dllや.soや.dylibといったライブラリを必ず開かないといけません。システムコールでもCの標準ライブラリの機能でも、この手順に則る必要があります。malloc()
やfree()
も例外ではありません。(この辺はjs-ctypesが機能として持っててくれてもいいと思うんですが、残念ながらそうはなっていません……)
ところで、MozillaのCのコードではCのmalloc()
やfree()
を直接呼ぶ代わりに、PR_Malloc()
やPR_Free()
という関数を使うようになってます(インターフェースはC標準のmalloc/freeと同じですが、malloc/freeで引数の型がsize_tになっている所が、これらのPR_なんちゃらではPRUint32になってるという点が異なります)。ということは、Firefox本体のバイナリには必ずこれらの関数が含まれているという事なので、それを呼んでやればどんな環境でも確実にmalloc/freeできると言えます。
実際にオープンするファイルはNSPR(Netscape Portable Runtime)のバイナリで、これはWindowsではnspr4.dll、Linuxではlibnspr4.so、Mac OS Xではlibnspr4.dylibというファイル名になっています。以下は、それらを自動的にオープンしてメモリ確保関係の関数を定義する例です。
const Cc = Components.classes;
const Ci = Components.interfaces;
Components.utils.import('resource://gre/modules/ctypes.jsm');
const OS = Cc['@mozilla.org/xre/app-info;1']
.getService(Ci.nsIXULAppInfo)
.QueryInterface(Ci.nsIXULRuntime)
.OS.toLowerCase();
// プラットフォームごとに適切なライブラリを開く
const gNspr4 = OS.indexOf('darwin') == 0 ?
ctypes.open('libnspr4.dylib') :
OS.indexOf('linux') == 0 ?
ctypes.open('libnspr4.so') :
OS.indexOf('win') == 0 ?
ctypes.open('nspr4.dll') :
undefined ;
if (typeof gNspr4 == 'undefined')
throw new Error('unknown platform');
const PR_Malloc = gNspr4.declare(
'PR_Malloc',
ctypes.default_abi,
ctypes.voidptr_t,
ctypes.uint32_t // size
);
const PR_Calloc = gNspr4.declare(
'PR_Calloc',
ctypes.default_abi,
ctypes.voidptr_t,
ctypes.uint32_t, // nelem
ctypes.uint32_t // elsize
);
const PR_Realloc = gNspr4.declare(
'PR_Realloc',
ctypes.default_abi,
ctypes.voidptr_t,
ctypes.voidptr_t, // ptr
ctypes.uint32_t // elsize
);
const PR_Free = gNspr4.declare(
'PR_Free',
ctypes.default_abi,
ctypes.void_t,
ctypes.voidptr_t // ptr
);
これを使って先の例を書き直すと、以下のようになります。
var selfUsage = 0;
var pagesCount = self.NumberOfEntries;
if (is64bit) pagesCount = pagesCount * 2;
var PSAPI_WORKING_SET_INFORMATION = new ctypes.StructType
('PSAPI_WORKING_SET_INFORMATION', [
{ NumberOfEntries : ULONG_PTR },
{ WorkingSetInfo : ctypes.ArrayType(ctypes.uint32_t, pagesCount) }
]);
// メモリを確保する。
var actualSelf = PR_Malloc(PSAPI_WORKING_SET_INFORMATION.size);
// 固定長の構造体へのポインタにキャストする。
var actualSelf = ctypes.cast(self, PSAPI_WORKING_SET_INFORMATION.ptr);
if (QueryWorkingSet(GetCurrentProcess(),
// 既にポインタなので「.address()」は不要。
actualSelf,
PSAPI_WORKING_SET_INFORMATION.size) != 0) {
// ポインタが指しているデータの内容にアクセスするには
// 「.contents」を使う。
let pages = actualSelf.contents.WorkingSetInfo;
let step = is64bit ? 2 : 1 ;
for (let i = is64bit ? 1 : 0; i < pagesCount; i += step) {
if (!(pages[i] & 0x100)) {
selfUsage++;
}
}
selfUsage = selfUsage * systemInfo.dwPageSize;
}
// メモリを解放する。
PR_Free(actualSelf);
actualSelf = undefined;
先の例と見比べると、どこを書き換えたかが分かりやすいと思います。
PR_Malloc()
やPR_Calloc()
で確保した領域は自動的なGCの管理対象外になるので、PR_Free()
で解放しないとこれはほんとにメモリリークになります。そうなるとComponents.utils.forceGC()
でもメモリが解放されなくなるので、freeは絶対に忘れないでください。というか、GCがちゃんと走るようなケースであればそっちに任せておいた方がいいです。今回の事例のように、何故かどうしてもGCが走ってくれないという場合の緊急避難と考えておいた方がいいでしょう。
GCがあるからメモリ管理のことを気にしなくていいはずのJavaScriptでこんな事をやってるというのが、だいぶ意味不明でウケますね。
おひさしぶりです。最後の皮肉のところだけ反応させてください。
通常ガーベージは富豪的な処理になるので、多くの言語系では極力後回しにする実装になっています。たとえばJavaやC#(というよりも.Net)などがそのようになっています。
つまりこれは仮想メモリ空間に余裕があれば、極力ガーベージは実行されない、ということを意味します。
ただ通常特定の言語系がメモリ空間をどんどん使っていって、他のプロセスの実行に悪影響を与えたり、スラッシングを発生させるなどの問題を回避するために、各言語系はガーベージ処理を発火させる最大メモリ容量が設定されています。あるいは設定可能になっています。
Javaや.Netでは強制的にガーベージを開始させるAPIがあります。もしFirefoxのJavaScriptエンジンに同様なAPIがあるならそれを利用するのもテです。しかしそれでもメモリヒープは保持されてると思いますが。
少なくともJavaScript、Java、C#などの中間言語で動作する言語の場合、メモリを解放したから本当に仮想メモリ空間が解放されたと考えるのはハマルもと、もしくは無駄な作業の増大のもとになります。
ガーベージ処理が発火しないことと、メモリーリークは区別して考えた方がよいです。メモリー・リークはCやC++レベル、つまりシステムレベルでメモリーが解放されていない状態です。Cでfreeするのを忘れているとかです。
以下余談です。
仮想アドレス空間のメモリブロックの歯抜け状態を防ぐために、各言語系は管理対象とするメモリ空間を広めに取っているはずです。あまりブラウザやJavaScriptコードがメモリを消費するからといって神経質になる必要はありません。
また各OSの仮想アドレッシングの仕組みを知れば、ブラウザやJavaScriptが使っているモリサイズに対して神経質になること自体意味がないことが理解できると思います。
それよりCレベルでメモリヒープやメモリブロックを予約できなくなる方がよほど深刻です。
> 通常ガーベージは富豪的な処理になるので、多くの言語系では極力後回しに
> ...
> ガーベージ処理が発火しないことと、メモリーリークは区別して考えた方がよいです。
という話は
> 第一Components.utils.forceGC()で強制的にGCしてみたらちゃんと消費メモリが減ります。また、GCは一般に重たい処理と言われているので、解放できるメモリの量が一定以上にならない限りは自動ではGCされないのかな? とも思ったのですが、ほっとくとFirefoxの消費メモリが1.8GBとかあり得ないことになったりもしてました。
という部分で語り終えてると思ってたのですが、見返してみると前提やら何やらの話を端折りすぎてたような気もしたので、その前後に説明を足したり「対策」の節を書き足したりしてみました。
の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2011-03-27_malloc.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。
writeback message: Ready to post a comment.