《Java 虚拟机原理与性能调优》3.2 对象布局:对象头、压缩指针与字段重排
一个 Loan 到底占多少内存?字段宽度相加是 36 字节,JOL 说是 48。本节用 JOL 0.17 打印 Book、Small、Padded、int[3]、Loan 与 FinePolicy 的对象布局,解释 mark word、压缩类指针、字段重排与 8 字节对齐,再用 -XX:-UseCompressedOops 对照,最后核算 100 万 Loan 的缓存预算。
tag
一个 Loan 到底占多少内存?字段宽度相加是 36 字节,JOL 说是 48。本节用 JOL 0.17 打印 Book、Small、Padded、int[3]、Loan 与 FinePolicy 的对象布局,解释 mark word、压缩类指针、字段重排与 8 字节对齐,再用 -XX:-UseCompressedOops 对照,最后核算 100 万 Loan 的缓存预算。
启动预热要等三件事都完成、数据库同时只能进十个任务、分批导出要四个线程每轮对齐。本节用三组真实运行对比 CountDownLatch、Semaphore 与 CyclicBarrier 的语义差异,给出 libraryd 的 StartupGate、DbConcurrencyGate 与 ExportBarrier 实现,以及各自的失效边界。
线上 RSS 比 -Xmx 多出 1 GB,钱花在哪?本节用 MemoryShape 复现 libraryd 的线程与内存形状,跑 -XX:NativeMemoryTracking=summary 与 jcmd VM.native_memory、VM.metaspace、GC.heap_info,逐项解释 reserved/committed/used,并给出堆、元空间、直接内存与线程栈的预算表。
借阅事件的审计写库不该拖慢请求线程。本节用 ArrayBlockingQueue 搭一条有界流水线,用 300 个事件、32 个槽位、2 个消费者的饱和实验展示背压与拒绝,再把优雅停止做成可观测的 pending、drained、abandoned 三个数。
方法调用在 JVM 里就是一个栈帧。本节用 javap -c 的真实输出拆开局部变量表、操作数栈、动态链接与异常表,让五种调用指令各出现一次,并逐指令走完 FinePolicy.DEFAULT.fineCents(20) 的 1000 分计算过程。
class 文件变成可用的类要经过加载、链接、初始化。本节用 -Xlog:class+load 与 jcmd VM.classloaders 的输出拆开这三步,实测 Class.forName 与 loadClass 的初始化差异、两个 URLClassLoader 加载同一 jar 的类身份冲突,并落到 libraryd 的 PluginClassLoader 与 ServiceLoader。
用 synchronized 包一个 HashMap 也能线程安全,为什么 libraryd 还要换 ConcurrentHashMap?本节用 800 万次原子与非原子写入、三组键规模的对比实验,说明争用点在哪里,再用 CopyOnWriteArrayList 实现借阅监听器,给出两者的适用边界。
用 javap -v -p 和 xxd 拆开 Book 与 FinePolicy 的 class 文件:magic 与 major 69、常量池的描述符与两槽常量、字段与方法表、Code/LineNumberTable/BootstrapMethods/Record 属性,并解释 record 的 toString/equals/hashCode 为何是 invokedynamic。
libraryd 需要一条借阅等待队列:没有可借副本时让请求有序等待,有人归还时唤醒其中一个。本节用超时、中断、条件队列三组实验对比 synchronized 与 ReentrantLock,手写一个基于 AQS 的 BooleanLatch,并给出 LoanWaitQueue 的完整实现与边界。
工具不是越多越好,每个工具只回答一类问题。本节给出 jcmd help 的真实命令清单与开销分级,实测 8 秒 JFR 记录的 summary、热点与分配视图,展示 GC 日志字段和 ManagementFactory 快照代码,并划清 JMC、MAT、async-profiler 的边界。
8 个线程各增 100 万次,普通 long 只数到 333 万。本节用真实计数实验比较 AtomicLong 与 LongAdder 的吞吐与适用面,手写一遍 CAS 循环,再用 Treiber 栈复现 ABA 并给出 AtomicStampedReference 的修法,最后落到 libraryd 的 LoanCounters 与 LoanMetrics。
同一份代码在两个堆大小下会给出不同结论。本节固定 JDK、CPU、堆、GC 与启动参数,用 VM.flags 和 GC.heap_info 实测 256 MB、1 GB、2 GB 与 -Xms≠-Xmx 的差异,验证 ActiveProcessorCount 对 GC/JIT 线程数的影响,并给出 collect.sh。
libraryd 的缓存刷新线程写了新值,请求线程却一直读旧的。本节用停止标志与 Dekker litmus 两组可复现实验,量化可见性与重排序,讲清 happens-before 的六条规则,再落到 LifecycleFlag 与 AvailabilityCache 的 volatile 发布写法。
libraryd 的延迟抬升不能靠猜。本节把「现象→假设→实验→证据→结论」拆成可执行步骤,用 jcmd 的 VM.info、VM.flags、GC.heap_info 真实输出演示字段怎么读,并给出症状到第一工具的对照表与四条研究路径的分工。
libraryd 想在不改 LibraryService 源码的前提下测量 borrow 耗时,Java Agent 是最小侵入的入口。本节实现 Premain-Class 与 Can-Retransform-Classes 清单,用 Byte Buddy 1.18.16 的 Advice 在方法进出打点,真实运行 -javaagent 并读取 agent: borrow took N µs。
第 3 章要改写 libraryd 的字节码,第一步不是选库,而是读懂编译器到底生成了什么。本节用 javap -c -p -v 拆开 FineCalculator 的常量池、栈机器指令和 invokedynamic,再用 ASM 9.11.1 统计方法指令、用 Byte Buddy 1.18.16 生成子类,并给出两者的选择边界。
libraryd 的 ISBN 校验规则要允许客户以 jar 形式追加,不能每次改规则都重新打包整个服务。本节从三层类加载器与双亲委派讲起,实现 PluginClassLoader 的 findClass 与 findResources,用 META-INF/services 和 ServiceLoader 真正加载一次插件 jar,并记录类加载器身份。
《Java 编程实战》全 55 篇,承接卷一,把 library 从内存 CLI 长成可部署的 libraryd 服务:多模块 Maven、PostgreSQL、JDBC 与连接池、事务与索引、Javalin、参数校验、线程池与异步、日志指标追踪、缓存、Testcontainers 与容器化。本页汇总 16 章 48 节的完整索引与学习路径。
libraryd 的每个仓储写方法都包着 begin/commit/rollback,复制到第三个实现时已经很难保持一致。本节用 JDK 动态代理把 TransactionManager.inTransaction 接到仓储接口上,处理默认方法、Object 方法和受检异常,并说明为什么代理只能代理接口、自调用为什么会绕过它。
libraryd 的借阅方法既要记审计,又要在目录服务短暂超时时重试,手写 try/catch 会把业务代码淹掉。本节定义 @Audit 与 @Retry 两个运行时注解,用反射读取 action 与重试参数,实现一个带退避的调用器,并说明保留级别、默认值和幂等性边界。