热部署、插件卸载之后类加载器回收不掉,常常是 ThreadLocal 在作怪。这篇用一个真实的插件类加载器复现泄漏,逐个验证哪些写法会泄漏、怎么修,并看过期条目与 InheritableThreadLocal 的行为。
插件的 ThreadLocal 里存了插件自己的类的实例(Ctx,1 MB),工作线程还活着,放弃对 ClassLoader 的引用后 GC:ClassLoader 被回收 = false(没有被回收 = 泄漏) 这时堆占用约 3 MB 关闭线程池、线程结束之后再 GC:ClassLoader 被回收 = true
存插件类的实例,不清理 工作线程活着时 GC:ClassLoader 被回收 = false 存 String(JDK 类型),不清理 工作线程活着时 GC:ClassLoader 被回收 = true 存插件类实例,卸载前在同一线程 remove() 工作线程活着时 GC:ClassLoader 被回收 = true 插件加载后没有写过 ThreadLocal 工作线程活着时 GC:ClassLoader 被回收 = true
线程里 ThreadLocal 引用置 null、GC 之后:堆占用 42 MB(40 MB 的值还在:键是弱引用,值是强引用,过期条目没有被清理) 同一线程再对别的 ThreadLocal 做 3 次 set/get 之后 GC:堆占用 42 MB 再创建 40 个 ThreadLocal 并 set(触发扩容与清理)之后 GC:堆占用 1 MB 对照:set 之后 remove():GC 后堆占用 1 MB
请求 A 里(主线程设值)第一次提交任务,线程池线程此时才被创建:任务里看到 InheritableThreadLocal = 请求 A 的用户,普通 ThreadLocal = null 请求 B(主线程把值改成「请求 B 的用户」)再提交任务,复用同一个池线程:任务里看到 请求 A 的用户(继承只发生在线程创建时,之后不会更新) 虚拟线程(每个任务新建)里 InheritableThreadLocal = 请求 C 的用户(新线程创建时继承了当前值) platform 线程 inheritInheritableThreadLocals(false):看到 null