{"id":50225896,"url":"https://github.com/fukuchi/jmemviz","last_synced_at":"2026-05-26T15:39:50.865Z","repository":{"id":359732217,"uuid":"1247282639","full_name":"fukuchi/jmemviz","owner":"fukuchi","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-23T06:56:12.000Z","size":62,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T08:19:20.575Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fukuchi.png","metadata":{"files":{"readme":"README.ja.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T05:39:47.000Z","updated_at":"2026-05-23T06:56:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fukuchi/jmemviz","commit_stats":null,"previous_names":["fukuchi/jmemviz"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/fukuchi/jmemviz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukuchi%2Fjmemviz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukuchi%2Fjmemviz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukuchi%2Fjmemviz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukuchi%2Fjmemviz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fukuchi","download_url":"https://codeload.github.com/fukuchi/jmemviz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukuchi%2Fjmemviz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33528086,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"ssl_error","status_checked_at":"2026-05-26T15:22:15.568Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-26T15:39:49.919Z","updated_at":"2026-05-26T15:39:50.859Z","avatar_url":"https://github.com/fukuchi.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jmemviz\n\n[Java Object Layout (JOL)](https://github.com/openjdk/jol) と\n`sun.misc.Unsafe` を使って **JVM ヒープ上のオブジェクトの実体** を観察する\n教育用ツール。\n\nふたつの使い方:\n\n1. **CLI モード** — `int` / `double` / `Integer` / `Double` などのレイアウトを\n   静的にコンソールへ説明出力。座学向け。\n2. **トレース録画 + ブラウザ再生** — 任意のコード片に `Jmemviz.track / snap`\n   を埋め込んで実行し、各 snap 時点のバイト列を JSON に記録。\n   付属のローカルサーバ + ブラウザビューアで **ステップごとの差分** を\n   ピンクハイライト表示。\n\n## Requirements\n\n- JDK 21\n- Maven 3.8+\n- Linux/macOS (Windows は未確認)\n\n## Quick start\n\n```bash\nmvn package\n\n# 1. CLI モード (静的レイアウト解説)\njava -jar target/jmemviz-0.1.0-SNAPSHOT.jar demo\n\n# 2. record → serve → browser (デフォルト)\njava -jar target/jmemviz-0.1.0-SNAPSHOT.jar\n#   = record-and-serve trace.json 8765\n# → http://127.0.0.1:8765/ がブラウザで開く\n```\n\nサブコマンド一覧:\n\n```\njmemviz demo                              # CLI 解説出力\njmemviz record [out.json]                 # トレースだけ書き出す\njmemviz serve  [trace.json] [port]        # 既存トレースを配信\njmemviz record-and-serve [out] [port]     # 全部いっぺん\njmemviz preprocess \u003cinput.java\u003e [output.java]   # @jmemviz マーカーを展開\n```\n\n## ソースプリプロセッサ\n\n`jmemviz preprocess` を使うと、通常の Java ソースに `// @jmemviz` コメントを\n書くだけで `track/snap` 呼び出しを自動注入できる。\n\n### マーカー一覧\n\n| マーカー | 場所 | 生成されるコード |\n|---|---|---|\n| `// @jmemviz record \"path\"` | 独立行 | `record(\"path\", () -\u003e {` |\n| `// @jmemviz end` | 独立行 | `});` |\n| `// @jmemviz snap \"ラベル\"` | 独立行 | `snap(\"ラベル\");` |\n| `// @jmemviz snap` | 独立行 | `snap(\"step N\");` (連番) |\n| `decl; // @jmemviz track` | 宣言行の末尾 | `track(\"変数名\", 変数名);` を次行に注入 |\n| `decl; // @jmemviz track 名前` | 宣言行の末尾 | `track(\"名前\", 名前);` を次行に注入 |\n\n`import static org.fukuchi.jmemviz.Jmemviz.*;` が未記載の場合は自動付与される。\n\n### 例\n\n```java\n// PointDemo.java (マーカー付き — そのままコンパイル可, 録画は行わない)\npublic class PointDemo {\n    public static void main(String[] args) {\n        // @jmemviz record \"trace.json\"\n\n        int[] xs = {257, 258, 259}; // @jmemviz track\n        // @jmemviz snap \"int[] xs = {257, 258, 259}\"\n\n        xs[0] = 0x99999999;\n        // @jmemviz snap \"xs[0] = 0x99999999\"\n\n        // @jmemviz end\n    }\n}\n```\n\n```bash\njava -jar jmemviz.jar preprocess PointDemo.java PointDemo_out.java\n```\n\n生成された `PointDemo_out.java` の差分:\n\n```java\nimport static org.fukuchi.jmemviz.Jmemviz.*;  // ← 自動付与\npublic class PointDemo {\n    public static void main(String[] args) {\n        record(\"trace.json\", () -\u003e {           // ← record() ラッパー\n\n        int[] xs = {257, 258, 259};\n        track(\"xs\", xs);                       // ← track() 注入\n        snap(\"int[] xs = {257, 258, 259}\");    // ← snap() 展開\n\n        xs[0] = 0x99999999;\n        snap(\"xs[0] = 0x99999999\");\n\n        });                                    // ← end 展開\n    }\n}\n```\n\n\u003e **Note**: マーカーの数を減らして「自動推論」する機能 (全ローカル変数を自動\n\u003e `track`、全 mutation 後に自動 `snap` など) は今後の拡張として検討中。\n\n## 録画 API\n\n```java\nimport static org.fukuchi.jmemviz.Jmemviz.*;\n\nrecord(\"trace.json\", () -\u003e {\n    int[] xs = {257, 258, 259, 260};\n    track(\"xs\", xs);\n    snap(\"initial\");\n\n    xs[0] = 0x99999999;\n    snap(\"after xs[0] = 0x99999999\");\n});\n```\n\n- `track(name, obj)` — 追跡対象を登録。同名で呼ぶと差し替え (再代入時用)。\n  内部で `System.identityHashCode(obj)` を呼んで mark word を安定化する\n  (観測自身が mark word を書換えてしまう Heisenberg 現象の回避)\n- `snap(label)` — 現在追跡中の全オブジェクトのバイト列を JOL の `sizeOf` 分\n  だけ `Unsafe.getByte` で読み、`label` 付きで snapshot に追加\n\n`RecordDemo` には現在以下のシナリオが入っている (全 11 ステップ):\n\n1. `int[]` の要素書き換え → 配列ヘッダ後ろの該当 4B だけハイライト\n2. `Integer` のボクシング + 再代入 → 別インスタンスになりバイト全体が一新\n3. `Point { int x; int y; }` のフィールド書換 → 該当オフセットだけ変化\n4. **`Rectangle { Point topLeft, bottomRight; }`** → Rectangle 本体には Point の\n   値は入らず **4B の oop 参照が 2 本** 並ぶだけ。Point の中身を書き換えても\n   Rectangle 本体は不変。`r.bottomRight = newPoint` で初めて Rectangle 内の\n   参照フィールドのバイトが変化する\n\n## デモが何を見せるか\n\n### (A) プリミティブ単体はヒープ上のオブジェクトではない\n\n`int` や `double` はスタック上の値 (またはクラスフィールドにインライン)\nとして存在し、ヘッダも GC も同一性もない。JOL は `Object` しか受け取ら\nないので、プリミティブを単独で見ることはできない。\n\n### (A') ただし容器に入った瞬間、ヒープに raw バイトで並ぶ\n\n```\nint[] {257, 258, 259, 260}\nraw bytes (32B):\n  01 00 00 00 00 00 00 00   ← mark word\n  08 27 00 00               ← klass ptr\n  04 00 00 00               ← array length = 4\n  01 01 00 00 02 01 00 00   ← 257, 258\n  03 01 00 00 04 01 00 00   ← 259, 260\n```\n\n16B 配列ヘッダの直後に **4B 値が連続して並ぶ**。`Integer` のような\nper-element ヘッダは無い。`double[]` も同様で、IEEE 754 倍精度が\n8B ずつそのまま並ぶ (`1.0` = `00 00 00 00 00 00 f0 3f`)。\n\n```\nclass Point { int x; double y; } のインスタンス (24B):\n  01 00 00 00 00 00 00 00   ← mark word\n  b0 fc 01 01               ← klass ptr\n  11 11 11 11               ← Point.x  (= 0x11111111)\n  1f 85 eb 51 b8 1e 09 40   ← Point.y  (= 3.14, IEEE 754)\n```\n\nクラスのフィールドとしても同じ。プリミティブは **「箱」** にならず\nインスタンスの本体にインラインで畳み込まれる。\n\n### (B) Integer は 16 バイトの「箱」\n\n```\nOFF  SZ   TYPE DESCRIPTION               VALUE\n  0   8        (object header: mark)     0x0000000000000001\n  8   4        (object header: class)    0x00025fd8\n 12   4    int Integer.value             257\nInstance size: 16 bytes\n```\n\n- 8 バイトの **mark word** (ハッシュ・ロック・GC age 等)\n- 4 バイトの **klass pointer** (圧縮 oops 有効時)\n- 4 バイトの **value** フィールド\n- 合計 16 バイト (4 バイト整数を運ぶためだけに!)\n\nraw バイト列を `Unsafe.getByte` で 16 バイト分読むと、末尾 4 バイトが\nリトルエンディアンで `01 01 00 00` = 257 = `0x101` として確認できる。\n\n### (C) Double は 24 バイト (padding 付き)\n\n```\nOFF  SZ     TYPE DESCRIPTION               VALUE\n  0   8          (object header: mark)\n  8   4          (object header: class)\n 12   4          (alignment/padding gap)\n 16   8   double Double.value              3.14\nInstance size: 24 bytes\n```\n\n8 バイト境界に揃えるため、value の手前に 4 バイトの padding が\n入る。Java の **ヘッダ + 値** モデルが整列要件で膨らむことを示す。\n\n### (D) Integer キャッシュ\n\n`Integer.valueOf(int)` は `-128..127` の範囲を静的キャッシュから返す。\nそのため:\n\n```\nvalueOf(100) == valueOf(100) ?  true   ← キャッシュ内\nvalueOf(200) == valueOf(200) ?  false  ← キャッシュ外、毎回 new\n```\n\n`Integer x = 100;` というオートボクシングも内部的には `valueOf` を呼ぶ\nので同じ挙動。アドレスを比較しているのではなく `==` で参照同一性を見て\nいる点に注意。\n\n### (E) int[N] vs Integer[N]: ボクシングの一括コスト\n\n1000 要素の配列で比較すると:\n\n```\nint[1000]      reachable bytes:   4016\nInteger[1000]  reachable bytes:  20016   (× 5.0)\n  └─ array itself   :   4016 bytes\n  └─ 1000 Integers  :  16000 bytes  (= 1000 × 16B)\n```\n\n- `int[]`: ヘッダ 16B + 値 4B × N が一塊。\n- `Integer[]`: 配列に並ぶのは 4B の **参照だけ**。値本体 (16B/個) は\n  ヒープのどこか別の場所にバラまかれる。**5倍** のメモリ + ポインタ\n  追跡 + GC 圧力がかかる。\n\n## トラブルシューティング\n\nJOL が起動時にこんな warning を出す:\n\n```\n# WARNING: Unable to get Instrumentation. Dynamic Attach failed.\n# WARNING | Compressed references base/shifts are guessed by the experiment!\n```\n\nこれは Serviceability Agent にアタッチできないため。アドレス表示\n(`addressOf`) は **推測値** になるが、レイアウト情報そのものは正確。\n正確なアドレスが必要なら:\n\n```bash\njava -Djol.tryWithSudo=true -jar target/jmemviz-0.1.0-SNAPSHOT.jar\n# または\necho 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope\n```\n\nまたは `-javaagent:path/to/jol-core-0.17.jar` で起動する。\n\n## 設計メモ / 落とし穴\n\n- **varargs trap**: `GraphLayout.parseInstance` は `Object... roots` を\n  取るため、`Integer[]` を素で渡すと「1000 個のルート」として展開\n  されてしまい、配列自身がカウントから漏れる。`(Object) arr` キャストで\n  「1個の配列ルート」に矯正する必要がある。`int[]` はプリミティブ配列\n  なので varargs に展開されず問題なし。\n- `sun.misc.Unsafe` を `Field.setAccessible(true)` 経由で取得している。\n  JDK 21 では `jdk.unsupported` モジュールが自動解決されるので追加\n  オプション不要。\n- `Integer` を強制的にキャッシュ外から作るために、敢えて非推奨の\n  `new Integer(int)` を使っている (`@SuppressWarnings`)。本番コードでは\n  `Integer.valueOf(...)` を使うべき。\n\n## アーキテクチャ\n\n```\n[ Demo code ]\n   Jmemviz.record(out, () -\u003e { ... track(), snap() ... })\n       │\n       │   ・JOL ClassLayout で field 注釈を構築\n       │   ・Unsafe.getByte(obj, offset) で raw バイト読出 (GC セーフ)\n       ▼\n[ trace.json ]   \u003c- 自己完結した JSON スキーマ\n       │\n       ▼\n[ JmemvizServer ]   com.sun.net.httpserver.HttpServer (JDK 同梱, 外部依存なし)\n       │   GET /            → classpath:/viewer/index.html\n       │   GET /trace.json  → 書き出した JSON\n       │   Desktop.browse(URI) でブラウザ自動起動\n       ▼\n[ browser viewer ]   差分計算は JS 側 (prevBytes 比較)\n```\n\n設計判断の要点:\n\n- **UI は Web ブラウザ**。小さな vanilla JS ビューア (フレームワーク・\n  ビルド工程なし) で trace を描画する。Swing/JavaFX に比べてトレースが\n  可搬な JSON として残るのも利点。\n- **スナップショット起動は明示的 `snap(label)`**。Java には `sys.settrace`\n  に相当する軽量フックが無い (JVMTI/JDI は重い)。教材用途では「教えたい\n  瞬間だけ撮る」方が認知負荷が低い。\n- **GC 対策**: `Unsafe.getByte(Object, long)` は HotSpot 内部で oop ハンドル\n  を再解決するため GC 安全。アドレス表示 (`addressOf`) は best-effort で\n  保存するが、オブジェクト同定には `name` を使う。\n- **mark word の安定化**: `track()` 時に `System.identityHashCode(obj)` を\n  呼んで hash を mark word に焼き込む。これをしないと、我々自身の\n  `safeRepr()` (= `toString()` 経由で hashCode を生成) が観測中にヘッダを\n  書換えてしまい、ステップ 0 → 1 で勝手にバイトが変わって見える。\n\n詳細は `~/.claude/plans/visualization-architecture-java-synchronous-cherny.md`。\n\n## 今後の拡張余地\n\n- **Java Agent (ASM) で自動撮影**: ユーザが `snap()` を挿入しなくても\n  毎行スナップショットを撮れるようにする。v1 と JSON 互換なので後付け可能\n- **トレースの diff モード**: 2 つの trace.json をビューアで並べる\n- **JDK 内部構造の教材化**: `String` の `coder`/`hash` の展開を強化\n  (`value` の byte[] は子 region として表示済み)、`ArrayList` の `elementData` も子 region として展開\n- **Compact Object Headers (Lilliput, JDK 24+) 対応**: ヘッダサイズを\n  ハードコードしている箇所 (`buildFields` の 8 + 4 = 12B) を JOL から\n  動的取得するように\n\n## ファイル\n\n| ファイル | 役割 |\n|---|---|\n| `pom.xml` | Maven 設定 (JDK 21, JOL 0.17, shade plugin で fat jar) |\n| `src/main/java/org/fukuchi/jmemviz/Main.java` | CLI dispatcher (demo / record / serve / record-and-serve / preprocess) |\n| `src/main/java/org/fukuchi/jmemviz/Jmemviz.java` | 公開 API (`record/track/snap`) + Snapshotter |\n| `src/main/java/org/fukuchi/jmemviz/Preprocessor.java` | ソースプリプロセッサ (`// @jmemviz` マーカー展開) |\n| `src/main/java/org/fukuchi/jmemviz/TraceWriter.java` | JSON 書き出し (手書き、依存なし) |\n| `src/main/java/org/fukuchi/jmemviz/JmemvizServer.java` | HttpServer + ブラウザ起動 |\n| `src/main/java/org/fukuchi/jmemviz/RecordDemo.java` | snap() を使った録画用デモ |\n| `src/main/java/org/fukuchi/jmemviz/JmemvizDemo.java` | 既存の CLI 解説デモ |\n| `src/main/java/org/fukuchi/jmemviz/examples/PointDemo.java` | プリプロセッサ用サンプル入力 (マーカー付き) |\n| `src/main/resources/viewer/index.html` | ブラウザビューア (vanilla JS, 依存なし) |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffukuchi%2Fjmemviz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffukuchi%2Fjmemviz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffukuchi%2Fjmemviz/lists"}