Java网页爬虫内存泄漏主因是缓存失控,Document、Response等对象被静态集合、单例或ThreadLocal长期持有;需用jstat/jmap/MAT定位强引用链,修复应替换静态Map为Caffeine缓存、清理ThreadLocal、精简缓存内容。

Java网页爬虫缓存网页时出现内存泄漏,核心问题往往不是“爬得太多”,而是“缓存没管住”——页面对象(如Document、Response、byte[])被静态集合、单例缓存或未清理的ThreadLocal长期持有,导致GC无法回收。排查要聚焦缓存生命周期与引用链,而非单纯看爬取量。
先确认是缓存泄漏,不是瞬时压力
用jstat盯住老年代行为,每2秒刷新:
- jstat -gcutil <pid> 2000:若O(老年代使用率)持续>85%且每次Full GC后仅下降1–3%,基线逐次抬高,基本锁定缓存类对象滞留
- jmap -histo <pid> | 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 <pid>(加live过滤已标记可回收对象,更干净)
- 用MAT打开,先看Dominator Tree,按Retained Heap排序,找到占用最大的缓存类(如CrawlerPageCache.map 或 static Map<String, Document>)
- 对该Map右键 → Path to GC Roots → exclude weak/soft references,重点看引用链顶端是不是:
• static字段(如private static final Map cache = new HashMap())
• Spring单例Bean的成员变量(Bean销毁了,Map还活着)
• ThreadLocal<Map>(爬虫线程复用,缓存随线程泄漏)
检查缓存设计的三个典型漏洞
代码层面快速过筛:
立即学习“Java免费学习笔记(深入)”;
- 只存不删:缓存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(); - 若必须用本地Map,加定时任务:
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(this::cleanExpired, 1, 1, MINUTES); - ThreadLocal缓存务必配
remove():在爬虫任务finally块或拦截器中显式调用threadLocal.remove() - 对Document等大对象,缓存前转成轻量结构(如提取title、url、text摘要),别缓存原始JSoup DOM树


















