真实计时对比两种异常:普通异常(真实填充调用栈)和用 writableStackTrace=false 构造的异常(跳过栈填充),30 层调用深度、50 万次,真实测出关掉栈追踪只快了约 1.3 倍,两次独立运行结果一致;这个数字远小于社区里流传的更夸张的说法,真实瓶颈更多在异常控制流本身,不是 fillInStackTrace 一个方法。
用上一篇写的 Instrumentation agent,真实累加 100 万个超出 IntegerCache 范围的装箱 Integer 对象各自的真实大小:合计约 15MB;同样 100 万个值放进 int[] 原生数组,真实只占约 3MB;装箱版本真实多占用约 11MB,倍数正好是 4.00——每个 int 值本身只要 4 字节,包装成 Integer 后连对象头带对齐一共要 16 字节。
写一个只有 premain 方法的 Java agent,用 Instrumentation.getObjectSize 真实测对象大小:空对象、只有一个 byte 字段、只有一个 int 字段都是真实 16 字节;只要加一个 long 字段就真实跳到 24 字节;不管字段声明顺序是 int 后 byte 还是 byte 后 int,真实测出来大小完全一样,因为 JVM 会重排字段顺序,不按声明顺序排。
反射进 StringBuilder 底层的 byte[] 数组,连续 append 2000 次,真实记录每次扩容后的新容量:16、34、70、142、286、574、1150、2302,完全对应 oldCapacity*2+2 这个公式;预先给够容量(new StringBuilder(2000))之后,同样 append 2000 次,真实测出扩容次数是 0。
拿一个 hashCode 依赖可变字段的 Key 放进 HashMap,插入后修改这个字段:真实测出 get、containsKey、remove 全部失败(null/false/null),但 map.size() 没变,entrySet 遍历依然能找到它;反射扫描全表定位到这个对象实际待的桶是 7 号,而 get() 修改字段后去找的桶变成了 14 号——桶变了,东西还在原地,这才是真正的原因。先 remove 再改字段再 put 回去,能真实恢复正常。
用一个 hashCode 永远返回 42 的坏类反射进 HashMap 的 table:真实测出桶内已有 8 个节点、插入第 9 个才真正树化;起始容量不到 64 时即使节点数早过 8,也只会连续扩容(真实测到 16 到 32 到 64),不会树化;remove 一路删到 0 个,类型全程真实保持 TreeNode,没有按常见说法在剩 6 个时转回链表——那是 resize 拆分时的逻辑,不是 remove 的逻辑。
真实踩坑:第一次实验线程跑完就退出,反射进去发现 threadLocals 字段直接是 null——因为 JVM 在线程真实退出时会自动清空这个字段,得让线程模拟线程池那样跑完活之后卡住待命,才测得到真实的残留;改用这个方式后,500 次创建 100KB 的值、不调用 remove(),真实反射进 Thread 的 ThreadLocalMap,数出 238 个 key 已被弱引用回收、但 value 依然真实占着内存的残留条目,合计真实吃掉约 23MB;每次用完调用 remove() 之后,残留条目和字节数都真实变成 0。
真实起 4 个线程各抢锁 2 万次,统计同一个线程连续拿到锁的最长次数——非公平锁真实测出最长连续 2346 次(同一个线程反复插队),公平锁只有 134 次;耗时上非公平锁真实只用 7ms,公平锁真实用了 411ms,慢了 58 倍;两次独立运行结果趋势一致,解释清楚公平锁为什么慢——每次都要老老实实唤醒排在队首的线程,而不是让刚释放锁的线程直接再抢一次。
真实起 16 个线程并发 put 80 万个不重复的 key 进普通 HashMap,最终 size() 真实只剩 656061,丢了将近 18%,没有抛任何异常;换成 ConcurrentHashMap 同样的并发写入,size() 真实精确等于 800000;真实验证锁粒度——一个线程在 keyA 上执行耗时 800ms 的 compute,另一个线程同时 put 完全不相关的 keyB,用 synchronizedMap 包装的 HashMap 真实被阻塞等了 697ms,换成 ConcurrentHashMap 真实 0 毫秒立刻返回,证明它锁的是单个桶而不是整张表。
真实反射读出 ArrayList 的 elementData 字段,证明刚 new 出来的空 ArrayList 共享同一个哨兵数组,第一次 add 才真实分配容量 10;真实扫描 200 次 add,验证扩容公式就是旧容量 + 旧容量右移一位(约 1.5 倍),10→15→22→33→49→73→109→163→244 逐个对上;真实添加 500 万个元素,不预设容量的版本底层数组真实重新分配了 34 次、累计搬运 12306801 个元素引用,预设容量的版本重新分配 0 次、搬运 0 个——用真实数字量化了能预估数据量就指定初始容量这条建议到底值多少。
真实起 8 个线程各累加 10 万次,plain int 和 volatile int 两种写法真实都把结果丢到只剩 15%~23%(期望 80 万,真实测出 111838~180720 不等,每次运行结果都不一样),只有 AtomicInteger 真实稳定跑出 800000;真实复现可见性问题——一个不带 volatile 的停止标志位,工作线程真实卡住超过 3 秒收不到停止信号,换成 volatile 后 0 毫秒立刻退出,两组数据对照着讲清楚 volatile 保证的到底是可见性还是原子性。
真实用反射读出 Integer 私有缓存类 IntegerCache 的 low/high 字段和 cache 数组,证明 -128~127 范围内的自动装箱确实复用同一个对象;真实扫描找到 == 从 true 翻转成 false 的精确边界正好是 128;真实复现一个把 == 当 equals 用的计数器 bug——用户 ID 小于 128 时测试环境一切正常,真实数据里 ID 一旦超过 128,重复访问计数真实变成了 0。