答案是:用jstack查runnable线程中是否反复出现getentry或transfer栈帧,结合mat分析heap.hprof确认next互指环。死循环表现为cpu 100%、无异常、多线程卡在e=e.next且地址交替,根源是jdk7扩容时头插法与竞态导致a↔b环。

看线程状态,锁定可疑栈帧
死循环发生时,JVM 进程不崩溃、无异常、无日志,但 CPU 持续 100%。此时用 jstack
重点关注以下两类栈帧:
-
java.util.HashMap.getEntry(...):说明线程正在遍历链表查找 key,若反复出现
e = e.next且栈深度异常增长(如几十层甚至上百层),极可能陷入环形链表无限跳转; - java.util.HashMap.transfer(...):说明线程正处在扩容迁移阶段,若多个线程同时卡在此处,尤其指向同一 HashMap 实例,说明并发 resize 已触发竞态窗口。
确认是否真为环形链表,而非其他高 CPU 原因
不能仅凭 CPU 高就断定是 HashMap 死循环。需排除 GC 频繁、外部服务阻塞、synchronized 锁竞争等干扰项。
判断环形链表的关键特征有:
详细的 Three.js 3D 图形参考,涵盖场景设置、相机、几何体、材质、光照、动画、控制器、加载器、数学工具和调试。
- 多个线程堆栈中,
e = e.next出现在连续多帧,且e和e.next地址反复交替(例如 A→B→A→B…); - 相同 HashMap 实例被多个线程同时访问,且调用链集中在
get()或put()的链表遍历路径上; - 线程长时间停留在同一个方法内,堆栈无明显变化,CPU 时间片持续消耗。
结合堆快照,验证 next 字段互指
jstack 只能提示线索,最终确认需依赖堆内存分析。
执行 jmap -dump:live,format=b,file=heap.hprof
- 搜索类 java.util.HashMap$Entry(JDK7)或 java.util.HashMap$Node(JDK8);
- 按 next 字段值排序,查找是否存在两个 Entry 对象,其
next字段互指(A.next == B,B.next == A); - 进一步检查这两个 Entry 所属的 HashMap 实例是否为同一对象,确认环发生在该 map 的某个桶中。
理解环路成因:头插法 + 竞态时序缺一不可
JDK7 中的环不是随机产生,而是特定执行顺序下的必然结果:
- 旧链表为 A→B→null,T1、T2 同时进入
transfer(),初始都持有 e=A、next=B; - T1 完成迁移:头插法使新表中变为 B→A→null;
- T2 被挂起后恢复,仍用旧局部变量 e=A、next=B,将 A 插入新表头,再将 B 插入新表头 → A.next = B,B.next = A;
- 环形成后,任何对这个桶的
get()都会陷入 A↔B 的无限循环。
这个过程依赖精确的线程调度时机和内存可见性缺失,所以压测中常需数百次尝试才能复现一次。










