更新履歴(4件・最終更新 2026年09月07日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 主張・コード・注意事項を維持し、全3回の読み順と、仮想メモリの割り当て・物理ページの状態遷移・共有とコピーオンライトの仕組みおよび観測方法を、小見出しと比較表で読みやすく整理した。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの、TLBがページフォルトを「防ぐ」かつ「両立しない」とする矛盾した2つの関係を、「TLBは有効なPTEの変換結果をキャッシュして利用する(保護違反はキャッシュ済みでもフォルトする)」という1つの関係に改めました。本文の説明は変えていません。
- 文章のリズムと段落構成を見直して読みやすくリライトし、仕組みの説明に図を追加しました。あわせて、保護チェックがTLBヒット時にも働くこと、ガードページ通知がページフォルトの結末の1つであることを本文と図に明記しました。コードと参考リンクは変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.22054246)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windowsメモリの深層(第1回) ── 仮想アドレスが物理RAMに変わる瞬間:ページフォルトの一部始終」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22054246 https://comcomponent.com/blog/windows-memory-internals-page-fault/
- DOI(最新版)
- 10.5281/zenodo.22054246
- DOI(この版)
- 10.5281/zenodo.22556623
「256MiBをCommitしたのに、Working Setは同じだけ増えない」。VirtualAllocを使ったときのこの疑問が、第1回の出発点です。
通常のプライベートメモリでは、Commitしたことと、そのページが物理RAMに置かれたことは別です。Windowsは、アプリが本当にページへ触れるまで、物理ページの割り当てを遅らせます。最初のアクセスでページフォルトが起きると、メモリマネージャーがVAD、PTE、保護属性、バックストアを調べ、必要なRAMを1ページずつ結び付けます。1
本記事では、この「最初に触った1バイト」が物理RAMへ到達するまでを追います。Working SetやCommitという数字の意味を先に整理したい方は、導入編「Windowsの『メモリ使用量』は何を表しているのか ── Working Set・Private Bytes・Commit・ページファイルを正しく読む」をご覧ください。本連載は用語を再定義するのではなく、なぜその数字になるのかを仕組みから掘り下げます。
「Windowsメモリの深層」全3回
本連載は、物理ページを得る → 常駐・回収の流れを追う → 共有と私有化を理解する、という順に進みます。
| 回 | テーマ | この回で追うこと |
|---|---|---|
| 第1回(本記事) | 仮想アドレスとページフォルト | VirtualAllocした領域が、いつ物理RAMを得るか |
| 第2回 | 物理ページの一生 | Working Setから外れたページの状態遷移と、ページファイルの役割 |
| 第3回 | セクションオブジェクトとコピーオンライト | DLL・ファイルマッピング・共有メモリが、どのように物理ページを共有するか |
第1回が答える疑問は、ただ1つです。
Commit済みの仮想アドレスは、どの瞬間に物理RAMへ変わるのか。
| 読み始める前に | 内容 |
|---|---|
| 対象読者 | メモリ使用量、起動直後のページフォルト、0xC0000005、VMMapやPerfMonの数字を仕組みから理解したい開発者・運用担当者 |
| 前提環境 | Windows 10/11または現行のWindows Server |
| 前提知識 | ポインターとVirtualAllocの基礎 |
| 難易度 | 中級。ページテーブルのビット配置やカーネルデバッガーの経験は不要 |
内部構造の名前は扱いますが、Windowsの特定ビルドに依存する非公開レイアウトは前提にしません。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全14件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論
通常のプライベートメモリでは、次の3段階を分けると、CommitとWorking Setの違いが見えてきます。
- Reserveは、仮想空間の住所を押さえる段階です。 その範囲を予約しますが、RAMやページファイルに物理的な保存領域は割り当てません。1
- Commitは、将来その内容を保持できるとシステムが約束する段階です。 コミット料金を課してCommit Totalを増やしますが、通常はまだ全ページに物理RAMを結び付けません。12
- Touchは、実際に物理ページが必要になる段階です。 初回アクセスでページフォルトが起き、正当なアクセスならメモリマネージャーが物理ページを割り当てて命令を再実行します。
MEM_COMMITは「RAMを今すぐ確保せよ」という命令ではありません。ただし空約束でもなく、将来その内容をRAMまたは適切なバックストアで保持できるという、システム全体の約束です。Commit済みページの初期内容がゼロであることと、物理ページが初回アクセスまで割り当てられないことは、両立します。1
また、「Reserve/CommitはVADへ書くだけ」という理解も不正確です。Reserveでは主に範囲と属性を表すVADが作られ、CommitではシステムのCommit Totalが増え、範囲のコミット状態が記録されます。ページテーブルの中間階層や個々のPTEは、必要になった時点で遅延構築されます。
| 知りたいこと | 読む節 |
|---|---|
| Reserve・Commit・Touchで何が変わるか | 2〜3節 |
| VAD・PTE・TLBは何を判断するか | 4〜6節 |
| 正常なフォルトとI/O待ち・例外を分けたい | 7〜9節 |
| 手元の数値で確かめたい | 10〜11節 |
flowchart TB
accTitle: Reserve、Commit、初回アクセスで起きること
accDescr: MEM_RESERVEはVADへ範囲と属性を記録し、MEM_COMMITはCommit Totalを消費して保存を約束し、初回アクセスのページフォルトが物理ページを割り当ててWorking Setへ加える
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. 初回アクセス(Touch)"]
reserve -.-> vad["VADへ範囲と属性を記録"]
commit -.-> charge["Commit Totalを消費(物理ページはまだない)"]
touch --> fault["ページフォルト"]
fault --> zero["ゼロ化済み物理ページをPTEへ結ぶ"]
zero --> ws["Working Setへ追加して命令を再実行"]
図1: Reserve、Commit、Touchは別の出来事。物理RAMが結び付くのは最後の初回アクセス時。
2. 仮想ページを追う3つの台帳
Windowsが持つ台帳は、同じメモリを違う単位で見ています。まず、範囲・仮想ページ・物理ページの3つを分けます。
| 台帳 | 単位 | 役割 |
|---|---|---|
| VAD | 仮想アドレス範囲 | 何の領域か、Reserve/Commit、保護、セクション対応を管理 |
| ページテーブル/PTE | 仮想ページ | 現在の物理ページへの変換、または未実体化状態を表現 |
| PFNデータベース | 物理ページ | 各RAMページの所有、参照、状態を追跡 |
VADは範囲、PTEは仮想ページ、PFNデータベースは物理ページの情報を持ちます。ページフォルトハンドラーは、これらを突き合わせてアクセスを続行できるかどうかを判断します。
flowchart TB
accTitle: 仮想アドレスから物理RAMまでの3つの台帳
accDescr: 仮想アドレスはVADが範囲単位で、PTEが仮想ページ単位で管理し、PTEの変換先である物理ページをPFNデータベースが物理ページ単位で追跡する
va["仮想アドレス"] --> vad["VAD(範囲の台帳)"]
va --> pte["PTE(仮想ページの台帳)"]
vad -.->|Reserve/Commit・保護を判定| pte
pte -->|有効な変換| pfn["PFNデータベース(物理ページの台帳)"]
pfn --> ram["物理RAMページ"]
図2: 粒度の違う3つの台帳。フォルト処理はVADとPTEを突き合わせ、結果をPFN側へ反映する。
本記事の主役はVADとPTEです。PFNデータベースは、第2回で物理ページの側から見ていきます。
3. Reserve、Commit、Touchは別の出来事
3.1. Reserve ── 住所を押さえる
まず、256MiBの連続した仮想アドレス範囲を予約してみます。
void* base = VirtualAlloc(
nullptr,
256ull * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
この時点で起きたのは、この範囲を他の割り当てに使わせないよう、プロセスの仮想空間に住所を確保したことだけです。MEM_RESERVEは、RAMにもページファイルにも物理的な保存領域を割り当てません。1
64bitプロセスでは広大な仮想空間を使えるため、大きな範囲を先にReserveしておき、必要な部分だけ後からCommitするという設計が現実的になります。
3.2. Commit ── 保存できることを約束する
次に、予約済みの範囲をCommitします。
void* committed = VirtualAlloc(
base,
256ull * 1024 * 1024,
MEM_COMMIT,
PAGE_READWRITE);
成功すると、システムのCommit Totalと、通常はプロセスのPrivate Bytesに反映される約束量が増えます。それでも、256MiB分の物理ページが一度に並ぶわけではありません。通常のページは、初回アクセスまで物理的に割り当てられないままです。12
では、Commitに何の意味があるのでしょうか。それは、システムが約束を引き受けられない場合に、メモリを使っている途中ではなくCommitの時点で失敗を返せることです。
3.3. Touch ── 物理ページが必要になる
最後に、次の代入で先頭ページへ初めて書き込みます。
static_cast<unsigned char*>(base)[0] = 1;
CPUは仮想アドレスを物理アドレスへ変換しようとしますが、PTEにはまだ有効な物理ページへの変換がありません。ここでページフォルトが発生します。
制御を受け取ったメモリマネージャーは、「Commit済みで書き込み可能なプライベートページへの初回アクセス」と判定し、ゼロ化済みの物理ページを確保してPTEへ結び、Working Setへ加えます。その後、失敗した書き込み命令をもう一度実行させます。
アプリから見えるのはただの代入ですが、内部では代入の途中でカーネルへ入り、物理ページを割り当て、同じ命令へ戻ってくるという処理が走っているのです。
4. VAD ── 仮想空間の範囲台帳
VADはVirtual Address Descriptorの略で、Windowsはプロセスの利用中アドレス範囲をVADの木として管理します。WinDbgの!vadコマンドを使うと、開始・終了VPN、Commit、保護属性、Private/Mapped、Control Areaなどを確認できます。3
VADが表す代表的な情報は次のとおりです。
- アドレス範囲の開始と終了
- Private、Mapped、Imageなどの種類
- Reserve/Commitの状態
- 読み取り、書き込み、実行、Copy-on-Writeなどの保護
- ファイルやセクションとの対応
- ガードページなどの特殊属性
範囲単位で持つ理由は、管理の無駄を減らすためです。 256MiBは、4KiBページに換算すると65,536ページになります。
最初から全ページに完全な管理構造を作るのではなく、VADで「この連続範囲は1つの予約」と管理します。そのうえで、必要になったページから具体化していきます。
4.1. VADが見つかれば必ず回復できるわけではない
「VADに載っていればフォルトを解決し、載っていなければアクセス違反」という説明は、入口としては便利ですが単純化しすぎています。VADが見つかっても、たとえば次のような場合には通常のアクセスを続けられません。
- Reserveのみで対象ページがCommitされていない
PAGE_NOACCESSである- 読み取り専用ページへ書き込んだ
- 実行不可ページから命令を実行した
- ガードページへ初めて触れた
- セクションの有効範囲外へ触れた
反対に、PTEが無効であっても、VADとPTEのソフトウェア状態から正当なアクセスだと分かれば、デマンドゼロ、Transition復帰、ページイン、CoWとして解決できます。正確に言えば、VAD、PTE、保護属性、アクセス種別を合わせて判定する、が答えです。
5. ページテーブルとTLB
アプリが持つポインターは仮想アドレスです。CPUがRAMへアクセスするには、仮想ページ番号を物理ページ番号へ変換しなければなりません。その階層的な変換表がページテーブルで、末端のエントリーがPTE(Page Table Entry)です。
有効なPTEは、概念的にはPFN、読み書き実行の保護、ユーザーモード可否、Accessed/Dirtyなどの情報を持ちます。実際のビット配置はCPUとWindowsのバージョンに依存します。
もっとも、毎回ページテーブルをたどるのは遅すぎるため、CPUは最近の変換結果をTLB(Translation Lookaside Buffer)へキャッシュしています。アドレス変換は次の順で進みます。
- TLBに変換があり、アクセスがその保護に適合すれば、その結果を使います。
- TLBに変換がなければ、CPUがページテーブルをたどります。
- 有効なPTEがあり、保護にも適合すれば、TLBへ登録して続行します。
- 有効な変換がない、または保護違反ならページフォルトの入口へ進みます。保護のチェックは、変換がTLBから来た場合にも行われます。
ここで混同しやすいのが、TLBミスとページフォルトは別物だという点です。
| 状況 | 次に起きること |
|---|---|
| TLBに変換があり、保護にも適合する | キャッシュした変換で続行する |
| TLBに変換はないが、有効なPTEがあり、保護にも適合する | ページテーブルウォークで変換を得て続行する |
| 有効な変換がない、または保護に違反する | ページフォルトの入口へ進む |
保護のチェックはTLBヒット時にも働きます。読み取り専用ページへの書き込みや、実行不可ページでの命令実行は、変換がキャッシュ済みでもフォルトします。CoWページへの書き込みがフォルトできるのも、このためです。
flowchart TB
accTitle: アドレス変換の流れとページフォルトの入口
accDescr: TLBに変換があっても保護に適合しなければページフォルトの入口へ進む。TLBに変換がなければページテーブルをたどり、有効なPTEで保護にも適合すればTLBへ登録して続行し、無効または保護違反のときはページフォルトの入口へ進む
access["メモリアクセス"] --> tlb{"TLBに変換がある?"}
tlb -->|ある| perm{"保護に適合する?"}
perm -->|適合| go["その変換で続行"]
perm -->|保護違反| entry["ページフォルトの入口へ"]
tlb -->|ない| walk["ページテーブルウォーク"]
walk --> valid{"有効なPTEで保護にも適合?"}
valid -->|はい| register["TLBへ登録して続行(フォルトなし)"]
valid -->|無効または保護違反| entry
図3: TLBミスはページテーブルウォークで解決できる。ページフォルトへ進むのは変換が無効か保護違反のときで、保護違反はTLBヒットでも起きる。
5.1. 無効なPTEは単なる空欄ではない
無効なPTEといっても、中身が空というわけではありません。Windowsは無効PTEのソフトウェア状態から、たとえば次のようなケースを区別します。
- 一度も実体化していないデマンドゼロページ
- RAMに残るTransitionページ
- Prototype PTEを参照する共有ページ
- ページファイルに保存されたプライベートページ
- 保護違反や無効領域
CPUの仕事は「通常の有効変換ではない」と判断してカーネルへ渡すところまでで、その先の意味付けはメモリマネージャーが行います。
6. ページフォルトの一部始終
Commit済みプライベートページへの初回書き込みを、6段階で追ってみましょう。
- CPUが書き込もうとする。
TLBとページテーブルを調べますが、対象PTEに有効なPFNがありません。 - CPUがページフォルトを発生させる。
フォルトした仮想アドレス、読み書き実行の種別、ユーザー/カーネル、変換不在か保護違反かをカーネルへ渡します。 - メモリマネージャーがVADとPTEを調べる。
Commit済みか、保護に合うか、デマンドゼロ・Transition・共有・ページイン・CoW・例外のどれかを決めます。 - デマンドゼロならゼロ化済み物理ページを得る。
他プロセスのデータを漏らさないため、新規に渡すページはゼロでなければなりません。 - PTEとPFN管理情報を更新する。
PTEへPFNと保護を設定し、物理ページをActiveにして、プロセスのWorking Setへ加えます。 - 失敗した命令を再実行する。
正常に解決したためユーザーモード例外は届かず、アプリは通常の代入として処理を続けます。
ETWのページフォルトイベントも、Transition、Demand Zero、Copy-on-Write、Guard Page、Hard Page Fault、Access Violationを別種として記録します。4
つまりページフォルトは、最初から「異常」を意味する言葉ではありません。CPUが通常経路で変換できなかったときに、OSへ判断を依頼するための共通入口なのです。
flowchart LR
accTitle: ページフォルトの解決先の分岐
accDescr: メモリマネージャーはVAD、PTE、保護属性、アクセス種別を判定し、デマンドゼロ、RAM内ページの再接続、バックストアからのハードフォルト、コピーオンライト、ガードページ通知、例外のいずれかへ振り分ける
faultIn["ページフォルト発生"] --> judge["VAD・PTE・保護・種別を判定"]
judge -->|初回アクセス| dz["デマンドゼロ(ソフト)"]
judge -->|RAM内に残存| soft["Standbyなどから再接続(ソフト)"]
judge -->|要ディスク読取| hard["ハードフォルト(ディスクI/O)"]
judge -->|CoW書き込み| cow["コピーしてPTEを差し替え"]
judge -->|ガードページ| guard["ガードを解除して通知"]
judge -->|解決不能| av["例外(0xC0000005など)"]
図4: 同じ入口から入ったフォルトが、判定結果によって6種類の結末へ分かれる。ガードページの詳細は第9節で扱う。
7. デマンドゼロ ── ディスクを読まないソフトフォルト
デマンドゼロは、Commit済みプライベートページへ初めて触れたときに起きる代表的なソフトフォルトです。MicrosoftのWorking Set解説も、「割り当て済み仮想ページをプロセスが初めて参照する」場合をソフトフォルトの例に挙げています。5
デマンドゼロには次の特徴があります。
- 元データをディスクから読む必要がない
- 初期内容はゼロ
- 利用可能な物理ページを結び付ける
- Working Setと累積Page Fault Countは増える
- この処理だけなら
Memory\\Pages Input/secは増えない
このため、起動直後にPage Faults/secが跳ね上がっても、それだけでストレージが詰まっているとは言えません。
7.1. 遅延割り当ては、RAMと初回アクセスのコストを交換する
256MiBをCommitしても、実際に使うのが8MiBだけなら、残り248MiBをRAMへ置かずに済む遅延割り当ては合理的です。その代わり、初回アクセスにはフォルト処理のコストが乗ります。
レイテンシが厳しい処理では、開始前に各ページへ触れる「プリフォールト」を選ぶこともあります。ただし、これは無料の最適化ではありません。初回アクセスの処理を先に済ませる代わりに、RAM常駐量も先に増やすという選択です。
8. ソフトフォルトとハードフォルト
判断の境目は、バックストアからの読み取りI/Oが必要かどうかです。ページフォルトの回数だけでは、この違いは分かりません。
8.1. ソフトフォルト
ソフトフォルトは、バックストアへの読み取りI/Oなしで解決できるフォルトです。代表例を挙げます。
- デマンドゼロ
- Standby/Transitionに残るページの再接続
- 他プロセスのWorking Setにある共有ページの接続
- 先読み済みページの接続
- 元ページが常駐しているCopy-on-Write
カーネル遷移、ロック、PTE/PFN更新、TLB整合などのCPUコストはかかりますが、ストレージ待ちはありません。5
8.2. ハードフォルト
一方、必要なページがRAMのどこにもなく、バックストアから読まなければならない場合がハードフォルトです。このとき読み取り元になるのは、ページファイルだけではありません。
- ページファイルへ退避されたプライベートページ
- メモリマップトファイル
- EXEやDLLのイメージ
- ファイルキャッシュが参照するデータファイル
ETWのHardFaultイベントにはFileObject、ReadOffset、ByteCountが含まれており、実際の読み取り元を追跡できます。6
したがって、Hard Fault = pagefile.sysを読んだ、ではありません。
バックストア読み取りが必要になると、要求はWindowsのI/Oスタックへ入ります。IRPと発行・完了の流れは「Windows I/Oの深層(第1回)」、ファイルキャッシュとの合流は「Windows I/Oの深層(第4回)」で扱っています。ページがRAMにあればメモリマネージャーだけで戻れますが、なければI/Oを発行し、完了までフォルトしたスレッドを待たせることになります。
9. 解決できないフォルトは例外になる
VADとPTEを調べても、正当な割り当て・ページイン・CoWとして解決できないフォルトは、ユーザーモードへ例外として届きます。
9.1. アクセス違反になる場合
その代表がSTATUS_ACCESS_VIOLATION、例外コード0xC0000005です。無効なアドレスの読み取り、書き込み、実行で発生し、第1例外パラメーターがアクセス種別、第2パラメーターが違反アドレスを示します。7
典型的な発生パターンは次のとおりです。
- NULL、解放後、配列外のアドレスを読む
- 読み取り専用ページへ書く
- DEP/NXで実行不可のページから命令を実行する
- CommitされていないReserve範囲へ触る
9.2. ガードページは一度だけの通知として使う
なお、PAGE_GUARDは少し違う意味を持ちます。アクセスを一度だけ通知する仕組みで、STATUS_GUARD_PAGE_VIOLATIONを発生させ、スタック拡張などに使われます。8
正常な遅延割り当ても、ページインも、CoWも、ガード通知も、最終的なアクセス違反も、CPUから見れば同じページフォルト入口へ集まります。結末を決めるのは、VAD、PTE、保護属性、アクセス種別の組み合わせです。
10. 自分の目で確かめる
実験の目的は、Commitが増える段階と、Working Setが増える段階を分けて観測することです。
次のC++プログラムは、256MiBをReserveし、Commitし、各ページへ1バイト書き、最後にReleaseします。各段階でEnterキーを待つので、そこでVMMapとPerfMonの値を確認します。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cstdlib>
#pragma comment(lib, "Psapi.lib")
constexpr SIZE_T kSize = 256ull * 1024 * 1024;
void PrintMemory(const char* stage)
{
PROCESS_MEMORY_COUNTERS_EX c{};
c.cb = sizeof(c);
if (!GetProcessMemoryInfo(
GetCurrentProcess(),
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&c),
sizeof(c))) {
std::printf("GetProcessMemoryInfo failed: %lu\n", GetLastError());
return;
}
std::printf(
"%-10s WS=%zu MiB Private=%zu MiB Faults=%lu\n",
stage,
c.WorkingSetSize / 1024 / 1024,
c.PrivateUsage / 1024 / 1024,
c.PageFaultCount);
}
void Pause(const char* message)
{
PrintMemory(message);
std::puts("Press Enter...");
(void)std::getchar();
}
int main()
{
SYSTEM_INFO si{};
GetSystemInfo(&si);
std::printf("PID=%lu, page=%lu bytes\n",
GetCurrentProcessId(), si.dwPageSize);
void* base = VirtualAlloc(nullptr, kSize, MEM_RESERVE, PAGE_NOACCESS);
if (!base) {
std::fprintf(stderr, "Reserve failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("reserved");
if (!VirtualAlloc(base, kSize, MEM_COMMIT, PAGE_READWRITE)) {
std::fprintf(stderr, "Commit failed: %lu\n", GetLastError());
VirtualFree(base, 0, MEM_RELEASE);
return EXIT_FAILURE;
}
Pause("committed");
auto* bytes = static_cast<volatile unsigned char*>(base);
for (SIZE_T offset = 0; offset < kSize; offset += si.dwPageSize) {
bytes[offset] = 1;
}
Pause("touched");
if (!VirtualFree(base, 0, MEM_RELEASE)) {
std::fprintf(stderr, "Release failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("released");
}
Visual Studioのx64 Native Tools Command Promptなら、次のコマンドでビルドできます。
cl /std:c++20 /EHsc /W4 memory_fault_demo.cpp
10.1. VMMapで見るもの
VMMapは、予約済み仮想メモリ、Commit、Working Set、Private、Shareableを種類別に表示するツールです。9 各段階で期待する変化は次のとおりです。
| 段階 | 期待する変化 |
|---|---|
| Reserve | Address SpaceのSizeは増えるが、Commit/WSは同量増えない |
| Commit | PrivateのCommitが約256MiB増える |
| Touch | Working SetとPrivate WSが大きく増え、Fault Countも増える |
| Release | 対象範囲が消え、CommitとWSが下がる |
数値の一致ではなく、段階間の変化を見る
実際の数値はランタイム、セキュリティ製品、メモリ圧力、観測タイミングで変わります。256MiBちょうどになるかどうかではなく、段階間でどちらへ動いたかを見てください。
10.2. PerfMonでソフト/ハードを分ける
PerfMonでは、次のカウンターを同じ時間軸へ並べます。
Process(<対象>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<対象>)\\Working Set - PrivateProcess(<対象>)\\Private Bytes
Process\\Page Faults/secにはソフトとハードの両方が含まれます。一方、Memory\\Pages Input/secは、ハードフォルト解決のためにディスクから読み込まれたページ数です。10
このプログラムのTouch段階では、Page Faults/secが跳ねてもPages Input/secは大きく増えないはずです。新規Commitページはデマンドゼロで実体化されるため、元データをディスクから読む必要がないからです。
同名プロセスはPIDでも確認する
なお、同名プロセスが複数ある場合、PerfMonのprocess#1などの番号は再起動で変わり得ます。PIDを表示するカウンターと突き合わせるか、Process V2やETW/WPAでPIDを使って識別してください。
11. 実務で避けたい3つの誤読
11.1. 「Commitが増えたからRAMリーク」
Commitは内容を保持する約束量であり、未TouchのページはRAMへ常駐していない場合があります。リークの判定では、Private Bytesの時間推移、割り当ての内訳、処理後に基準値へ戻るかどうかを見ます。
11.2. 「Page Faults/secが高いからディスクが遅い」
ソフトフォルトはディスクI/Oを伴いません。Page Faults/sec、Pages Input/sec、ストレージ待ちを分けて見て、必要ならETWのHardFaultイベントで読み取り元ファイルとスタックまで追います。
11.3. 「Working Setを空にすればリークが直る」
Working Setから外しても、Commitや所有権は解放されません。ページはStandbyやModifiedへ移り、後でフォルトして戻ってくるだけです。リークを直すには、割り当て元がVirtualFree、ヒープ解放、オブジェクト破棄などを行う必要があります。
外されたその物理ページがどこへ行くのかは、第2回で追います。
12. まとめ
MEM_RESERVEは仮想アドレス範囲を押さえますが、RAMやページファイルの物理領域を割り当てません。1MEM_COMMITはCommitを消費し、将来内容を保持できることを保証しますが、通常の物理ページは初回アクセスまで割り当てられません。12- VADは範囲、PTEは仮想ページ、PFNデータベースは物理ページの台帳です。
- TLBミスはページフォルトではありません。有効なPTEがあればページテーブルウォークだけで解決します。
- デマンドゼロ、Transition復帰、共有ページ接続は、ディスクI/Oなしで解決できるソフトフォルトです。5
- ページファイル、DLL、EXE、マップドファイルから読む必要があればハードフォルトです。6
- VAD、PTE、保護属性を検査して解決不能なら、
0xC0000005などの例外になります。7 - 性能判断では
Page Faults/sec単独ではなく、Pages Input/sec、Available、Working Set、Private Bytes、ストレージ待ちを同じ時間軸で見ます。
続きは第2回「物理ページの一生:5つのリストとページファイルの真実」です。
Commitという約束を物理ページへ変えた後、そのページがWorking Setから外れたらどこへ行くのかを、PFNデータベースとページリストから追います。
関連記事
- Windowsの「メモリ使用量」は何を表しているのか ── Working Set・Private Bytes・Commit・ページファイルを正しく読む
- Windows I/Oの深層(第1回) ── WindowsのI/OアーキテクチャとIRP
- Windows I/Oの深層(第4回) ── キャッシュマネージャーとWriteFile
- WinDbg + SOSでクラッシュダンプ解析
- Windowsアプリのクラッシュダンプ収集入門
関連する相談領域
合同会社小村ソフトでは、Windowsアプリケーションのメモリ使用量調査、アクセス違反、起動遅延、ページング、ネイティブコードの不具合解析を扱っています。
参考リンク
-
Microsoft Learn, VirtualAlloc function.
MEM_RESERVEが物理ストレージを割り当てずに仮想アドレス範囲を予約すること、MEM_COMMITがシステム全体のメモリとページファイルに対するコミット料金を課すこと、Commit済みページの初期内容がゼロであること、実際の物理ページはアクセスされるまで割り当てられないことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, PERFORMANCE_INFORMATION structure.
CommitTotalが現在のシステムCommitページ数、CommitLimitがページファイルを拡張せずにCommitできる上限であることについて。 ↩ ↩2 ↩3 -
Microsoft Learn, !vad (WinDbg).
!vadがVADツリーを表示し、開始・終了VPN、Commit、Mapped/Private、保護属性、Control Areaなどを確認できることについて。 ↩ -
Microsoft Learn, PageFault_TypeGroup1 class. ETWがTransition Fault、Demand Zero Fault、Copy-on-Write、Guard Page Fault、Hard Page Fault、Access Violationを区別して記録することについて。 ↩
-
Microsoft Learn, Working Set. ソフトフォルトがバックストアへのアクセスなしに解決でき、他プロセスのWorking Set、Transition、初回参照のデマンドゼロなどで起きることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. HardFaultイベントがFileObject、ReadOffset、ByteCount、VirtualAddress、Thread IDを含み、読み取り元を追跡できることについて。 ↩ ↩2
-
Microsoft Learn, Access Violation C0000005.
0xC0000005が無効なメモリアドレスの読み取り・書き込み・実行で発生し、例外パラメーターがアクセス種別と違反アドレスを示すことについて。 ↩ ↩2 -
Microsoft Learn, Creating Guard Pages.
PAGE_GUARDがページアクセスのワンショット通知を提供し、STATUS_GUARD_PAGE_VIOLATIONを発生させることについて。 ↩ -
Microsoft Learn, VMMap - Sysinternals. VMMapがCommit済み仮想メモリを種類別に分解し、各種類のWorking Setと詳細アドレスマップを表示できることについて。 ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool.
Memory\\Pages Input/secがハードページフォルト解決のためディスクから読み込まれたページ数であることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsメモリの深層(第2回) ── 物理ページの一生:5つのリストとページファイルの真実
PFNデータベース、Standby、Modified、メモリ圧縮、ページファイルをつなぎ、Working Setから外れた物理ページの行き先を解説します。
Windowsの「メモリ使用量」は何を表しているのか ── Working Set・Private Bytes・Commit・ページファイルを正しく読む
タスクマネージャーのメモリ、Working Set、Private Bytes、Commitは同じ値ではありません。Windowsの仮想メモリと物理メモリの関係、ページファイルの役割、メモリ不足やリーク調査で見るべき指標を解説します。
Windowsメモリの深層(第3回) ── セクションオブジェクトとコピーオンライト:DLLとファイルマッピングの正体
セクションオブジェクト、イメージ/データマッピング、共有キャッシュ、コピーオンライトをつなぎ、DLLや共有メモリが物理ページを共有する仕組みを解説します。
MS14-068で普通のユーザーが管理者になれた理由 ── KerberosのPACと署名検証
MS14-068(CVE-2014-6324)では、一般ユーザーが偽ったグループ情報をKDCが受け入れ、ドメイン管理者として扱われ得ました。PAC、鍵付き署名とチェックサムの違い、権限への反映、修正と検知の限界を解説します。
CurveBall――偽の証明書を、なぜWindowsが信頼してしまったのか? ── CryptoAPIと楕円曲線のパラメーター
CurveBall(CVE-2020-0601)は、偽の証明書を信頼された認証局のものと取り違えるWindowsの脆弱性でした。公開鍵の点と基準点の関係から、署名の計算が正しくても信頼の判定が崩れる理由を解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- VirtualAllocでMEM_COMMITした時点でRAMは確保されますか?
- 通常のプライベートメモリでは、Commit時にシステムのコミット余力は消費しますが、対応する物理ページは最初にアクセスされるまで割り当てられません。書き込みで初めて触れたページは、デマンドゼロフォルトの処理中に物理ページを得ます。
- ページフォルトは異常や性能問題を意味しますか?
- いいえ。デマンドゼロやStandbyからの復帰など、ディスクI/Oを伴わないソフトフォルトは通常動作です。性能判断ではPage Faults/secだけでなく、Pages Input/sec、ストレージ待ち、Available MBytesも合わせて見ます。
- TLBミスとページフォルトは同じですか?
- 別物です。TLBに変換結果がなくても、ページテーブルのPTEが有効ならCPUが表をたどって変換を再登録するだけです。PTEが無効、または保護違反があるときにページフォルトの入口へ進みます。
- VADにアドレス範囲があればアクセス違反にはなりませんか?
- そうとは限りません。VADの有無に加え、ReserveかCommitか、読み書き実行の保護属性、ガードページ、PTEの状態などをメモリマネージャーが評価します。解決不能なら0xC0000005などの例外になります。
- Page Faults/secが多いとRAM不足ですか?
- それだけでは判断できません。Page Faults/secには大量のソフトフォルトも含まれます。Memory\Pages Input/sec、Memory\Page Reads/sec、Available MBytes、ディスク待ち時間と同じ時間軸で相関を見る必要があります。