java网页爬虫内存泄漏主因是缓存失控,document、response等对象被静态集合、单例或threadlocal长期持有;需用jstat/jmap/mat定位强引用链,修复应替换静态map为caffeine缓存、清理threadlocal、精简缓存内容。

Java网页爬虫缓存网页时出现内存泄漏,核心问题往往不是“爬得太多”,而是“缓存没管住”——页面对象(如Document、Response、byte[])被静态集合、单例缓存或未清理的ThreadLocal长期持有,导致GC无法回收。排查要聚焦缓存生命周期与引用链,而非单纯看爬取量。
先确认是缓存泄漏,不是瞬时压力
用jstat盯住老年代行为,每2秒刷新:
-
jstat -gcutil
2000 :若O(老年代使用率)持续>85%且每次Full GC后仅下降1–3%,基线逐次抬高,基本锁定缓存类对象滞留 -
jmap -histo
| head -20 :重点查org.jsoup.nodes.Document、okhttp3.Response、byte[]、java.util.HashMap等——如果它们实例数在10分钟内翻倍,且总大小占堆30%以上,就是高危信号 - 观察日志是否频繁出现GC overhead limit exceeded,重启后1小时内内存又快速逼近阈值,这是典型的缓存“慢性堆积”特征
抓堆快照,定位缓存容器和强引用源头
在内存高位时执行:
-
jmap -dump:live,format=b,file=heap.hprof
(加live过滤已标记可回收对象,更干净) - 用MAT打开,先看Dominator Tree,按Retained Heap排序,找到占用最大的缓存类(如CrawlerPageCache.map 或 static Map
) - 对该Map右键 → Path to GC Roots → exclude weak/soft references,重点看引用链顶端是不是:
• static字段(如private static final Map cache = new HashMap())
• Spring单例Bean的成员变量(Bean销毁了,Map还活着)
• ThreadLocal
检查缓存设计的三个典型漏洞
代码层面快速过筛:
- 只存不删:缓存put后无remove、无LRU淘汰、无TTL过期(哪怕用了ConcurrentHashMap,key不消失,value就永远钉在堆里)
- Key带长生命周期对象:比如用完整HttpResponse做key,里面含InputStream、Socket引用,整个响应体图都被拖住
- 缓存与爬虫上下文耦合:把Document缓存在ThreadLocal中,但爬虫线程池未清理ThreadLocal(尤其用Executors.newCachedThreadPool时极易发生)
修复建议:轻量落地,避免大改
不一定要重写整个缓存模块:
- 静态Map立即替换为
Cache<string document> cache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(5, MINUTES).build();</string> - 若必须用本地Map,加定时任务:
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(this::cleanExpired, 1, 1, MINUTES); - ThreadLocal缓存务必配
remove():在爬虫任务finally块或拦截器中显式调用threadLocal.remove() - 对Document等大对象,缓存前转成轻量结构(如提取title、url、text摘要),别缓存原始JSoup DOM树
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











