迭代开始后真实往 CopyOnWriteArrayList 里塞两个新元素,迭代器真实只看到原来的 5 个,没有抛出 ConcurrentModificationException;对照普通 ArrayList 同样操作真实抛出异常。另一组真实计时测出 add 单次耗时随当前长度增长:size≈100 时约 627ns,size≈10000 时涨到约 7694ns——每次写操作都要真实把底层数组整个复制一份,这是快照语义的真实代价。
用 accessOrder=true 构造 LinkedHashMap,重写 removeEldestEntry 做一个容量为 3 的缓存:真实验证插入 1,2,3 后迭代顺序是 [1,2,3],访问 get(1) 之后迭代顺序真实变成 [2,3,1],再插入第 4 个,真实淘汰的是 2(最久没被访问的),不是 1(最早插入的)。第二组实验用连续访问制造更明确的顺序,真实验证淘汰结果和预期完全一致。
放 10000 个没有其它强引用的 key 进 WeakHashMap,gc 之前 size() 真实是 10000;触发两次 System.gc() 之后,第一次调用 size() 就真实变成了 0,不需要额外调用任何清理方法;对照组里故意保留 200 个 key 的强引用,GC 之后 size() 真实剩下正好 200 个,一个不多一个不少。
在 finalize() 里把 this 重新赋给一个静态字段,真实验证对象复活:resurrected 字段真实变成非 null,对象确实活了过来;但同时创建的 WeakReference 真实测出已经被清空,拿不到对象了,因为 GC 清理弱引用发生在运行 finalize 之前,弱引用一旦被清空就不会因为后来的复活而恢复;再对照 Cleaner,清理动作根本拿不到 this,真实验证没有复活的可能。
搭一个容量为 1 的有界缓冲区,4 个生产者 + 4 个消费者共享同一把锁,共 8 万个任务;用 notify() 真实跑 3 次,3 次全部在 5 秒超时内真实卡死,只完成 2、2、4 个;换成 notifyAll(),3 次全部真实在 1 秒内完成全部 8 万个——notify() 唤醒了错误类型的线程之后,双方就永久卡在 wait() 上,这就是真实的丢失唤醒。
真实计时对比 synchronized 的两种场景:单线程无竞争连续 4000 万次自增,和 8 线程竞争同一把锁共 4000 万次自增;真实测出无竞争约 215ms,8 线程竞争约 2084ms,倍数约 9.7;两次独立运行倍数分别是 9.69 和 8.74,结果 count 都正确,没有丢失更新——真实代价主要来自线程阻塞、唤醒和上下文切换。
用 javap -c 真实反编译一段 a+b+c 的字节码,发现 Java 9 之后编译器根本不生成 new StringBuilder,而是生成一条 invokedynamic 调用 makeConcatWithConstants;循环里 s = s + i 的每一次迭代也是独立的 invokedynamic 调用,真实计时测出比显式 StringBuilder.append 慢了约 400 倍,所以别在循环里用 + 这条建议仍然成立,只是原因变了。
真实计时对比两种异常:普通异常(真实填充调用栈)和用 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 回去,能真实恢复正常。