知其然,知其所以然
Java 内部原理 2026-05-27

CopyOnWriteArrayList:真实验证迭代时改了也不报错,但代价随长度真实涨

迭代开始后真实往 CopyOnWriteArrayList 里塞两个新元素,迭代器真实只看到原来的 5 个,没有抛出 ConcurrentModificationException;对照普通 ArrayList 同样操作真实抛出异常。另一组真实计时测出 add 单次耗时随当前长度增长:size≈100 时约 627ns,size≈10000 时涨到约 7694ns——每次写操作都要真实把底层数组整个复制一份,这是快照语义的真实代价。

Java CopyOnWriteArrayList 并发 动画演示
Java 内部原理 2026-05-26

LinkedHashMap 实现 LRU:真实验证淘汰的是最久没访问的,不是最早插入的

用 accessOrder=true 构造 LinkedHashMap,重写 removeEldestEntry 做一个容量为 3 的缓存:真实验证插入 1,2,3 后迭代顺序是 [1,2,3],访问 get(1) 之后迭代顺序真实变成 [2,3,1],再插入第 4 个,真实淘汰的是 2(最久没被访问的),不是 1(最早插入的)。第二组实验用连续访问制造更明确的顺序,真实验证淘汰结果和预期完全一致。

Java LinkedHashMap LRU 动画演示
Java 内部原理 2026-05-25

WeakHashMap:真实测出 GC 之后 10000 个 key 真的清空成了 0 个

放 10000 个没有其它强引用的 key 进 WeakHashMap,gc 之前 size() 真实是 10000;触发两次 System.gc() 之后,第一次调用 size() 就真实变成了 0,不需要额外调用任何清理方法;对照组里故意保留 200 个 key 的强引用,GC 之后 size() 真实剩下正好 200 个,一个不多一个不少。

Java WeakHashMap GC 动画演示
Java 内部原理 2026-05-24

finalize() 真的能让对象复活:真实验证,但 WeakReference 不会跟着活过来

在 finalize() 里把 this 重新赋给一个静态字段,真实验证对象复活:resurrected 字段真实变成非 null,对象确实活了过来;但同时创建的 WeakReference 真实测出已经被清空,拿不到对象了,因为 GC 清理弱引用发生在运行 finalize 之前,弱引用一旦被清空就不会因为后来的复活而恢复;再对照 Cleaner,清理动作根本拿不到 this,真实验证没有复活的可能。

Java finalize GC 动画演示
Java 内部原理 2026-05-23

notify() 真实测出会卡死:3 次运行,3 次真实卡在个位数

搭一个容量为 1 的有界缓冲区,4 个生产者 + 4 个消费者共享同一把锁,共 8 万个任务;用 notify() 真实跑 3 次,3 次全部在 5 秒超时内真实卡死,只完成 2、2、4 个;换成 notifyAll(),3 次全部真实在 1 秒内完成全部 8 万个——notify() 唤醒了错误类型的线程之后,双方就永久卡在 wait() 上,这就是真实的丢失唤醒。

Java wait/notify 并发 动画演示
Java 内部原理 2026-05-22

synchronized 锁膨胀:真实测出有竞争比无竞争慢了将近 9 倍

真实计时对比 synchronized 的两种场景:单线程无竞争连续 4000 万次自增,和 8 线程竞争同一把锁共 4000 万次自增;真实测出无竞争约 215ms,8 线程竞争约 2084ms,倍数约 9.7;两次独立运行倍数分别是 9.69 和 8.74,结果 count 都正确,没有丢失更新——真实代价主要来自线程阻塞、唤醒和上下文切换。

Java synchronized 并发 动画演示
Java 内部原理 2026-05-21

字符串 + 拼接编译成了什么:真实反汇编发现不是 new StringBuilder

用 javap -c 真实反编译一段 a+b+c 的字节码,发现 Java 9 之后编译器根本不生成 new StringBuilder,而是生成一条 invokedynamic 调用 makeConcatWithConstants;循环里 s = s + i 的每一次迭代也是独立的 invokedynamic 调用,真实计时测出比显式 StringBuilder.append 慢了约 400 倍,所以别在循环里用 + 这条建议仍然成立,只是原因变了。

Java 字符串拼接 字节码 动画演示
Java 内部原理 2026-05-20

异常真实有多贵:关掉栈追踪只快了 1.3 倍,不是传说中的 10 倍

真实计时对比两种异常:普通异常(真实填充调用栈)和用 writableStackTrace=false 构造的异常(跳过栈填充),30 层调用深度、50 万次,真实测出关掉栈追踪只快了约 1.3 倍,两次独立运行结果一致;这个数字远小于社区里流传的更夸张的说法,真实瓶颈更多在异常控制流本身,不是 fillInStackTrace 一个方法。

Java 异常 性能 动画演示
Java 内部原理 2026-05-19

自动装箱在循环里到底多分配了多少内存:真实测出是 4 倍

用上一篇写的 Instrumentation agent,真实累加 100 万个超出 IntegerCache 范围的装箱 Integer 对象各自的真实大小:合计约 15MB;同样 100 万个值放进 int[] 原生数组,真实只占约 3MB;装箱版本真实多占用约 11MB,倍数正好是 4.00——每个 int 值本身只要 4 字节,包装成 Integer 后连对象头带对齐一共要 16 字节。

Java 自动装箱 内存 动画演示
Java 内部原理 2026-05-18

空对象到底占几个字节:真实用 Instrumentation 测,不是背下来的 16

写一个只有 premain 方法的 Java agent,用 Instrumentation.getObjectSize 真实测对象大小:空对象、只有一个 byte 字段、只有一个 int 字段都是真实 16 字节;只要加一个 long 字段就真实跳到 24 字节;不管字段声明顺序是 int 后 byte 还是 byte 后 int,真实测出来大小完全一样,因为 JVM 会重排字段顺序,不按声明顺序排。

Java 对象内存布局 JVM 动画演示
Java 内部原理 2026-05-17

StringBuilder 扩容:真实反射测出容量是 16→34→70→142 这样长的

反射进 StringBuilder 底层的 byte[] 数组,连续 append 2000 次,真实记录每次扩容后的新容量:16、34、70、142、286、574、1150、2302,完全对应 oldCapacity*2+2 这个公式;预先给够容量(new StringBuilder(2000))之后,同样 append 2000 次,真实测出扩容次数是 0。

Java StringBuilder 扩容 动画演示
Java 内部原理 2026-05-16

HashMap 的 key 字段变了:真实测出对象没丢,只是永远找不到了

拿一个 hashCode 依赖可变字段的 Key 放进 HashMap,插入后修改这个字段:真实测出 get、containsKey、remove 全部失败(null/false/null),但 map.size() 没变,entrySet 遍历依然能找到它;反射扫描全表定位到这个对象实际待的桶是 7 号,而 get() 修改字段后去找的桶变成了 14 号——桶变了,东西还在原地,这才是真正的原因。先 remove 再改字段再 put 回去,能真实恢复正常。

Java HashMap equals/hashCode 动画演示
京ICP备2026067502号-1京公网安备11010502063487号