Java xpath 内存泄漏?

5

我遇到了一个困扰我数月的问题:我一直遇到OOM异常(堆空间),在检查堆转储时,我发现了数百万个对象实例,这些对象实例我从未分配过,但很可能是在底层库中分配的。经过大量的努力和汗水,我成功地定位了生成内存泄漏的代码,并编写了一个最小、完整和可验证的代码示例来说明:

import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.web.WebEngine;
import javafx.stage.Stage;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class MVC extends Application implements ChangeListener<Worker.State>{

    private final WebEngine engine = new WebEngine();
    private final String url = "https://biblio.ugent.be/publication?sort=publicationstatus.desc&sort=year.desc&limit=250&start=197000";
    private final XPath x = XPathFactory.newInstance().newXPath();

    @Override
    public void start(Stage primaryStage) throws Exception {
        System.setProperty("jsse.enableSNIExtension", "false");
        engine.getLoadWorker().stateProperty().addListener(this);
        engine.load(url);
    }

    public static void main(String[] args) {
        launch(args);
    }

    private NodeList eval(Node context, String xpath) throws XPathExpressionException{
        return (NodeList)x.evaluate(xpath, context, XPathConstants.NODESET);
    }

    @Override
    public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
        if (newValue==Worker.State.SUCCEEDED) {
            try {
                while(true){
                    NodeList eval = eval(engine.getDocument(), "//span[@class='title']");
                    int s = eval.getLength();
                }
            } catch (XPathExpressionException ex) {
                Logger.getLogger(MVC.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

该代码执行以下操作:
  • 使用JavaFX WebEngine加载文档。
  • 无限期地使用javax.xml包在文档上执行xpath查询,不存储结果或指针
要运行,请创建一个JavaFX应用程序,在默认包中添加名为MVC.java的文件,输入代码并点击运行。任何分析工具(我使用VisualVM)都应该很快地显示堆在几分钟内不受控制地增长。似乎分配了以下对象但从未释放:
  • java.util.HashMap$Node
  • com.sun.webkit.Disposer$WeakDisposerRecord
  • com.sun.webkit.dom.NamedNodeMapImpl$SelfDisposer
  • java.util.concurrent.LinkedBlockingQueue$Node
这种行为每次运行代码时都会发生,无论我加载的url或在文档上执行的xpath是什么。
我测试的设置如下:
  • MBP运行OS X Yosemite(最新版本)
  • JDK 1.8.0_60
有人能够重现这个问题吗?这是实际的内存泄漏吗?我能做些什么吗? 编辑 我的一位同事在装有JDK 1.8.0_45的w7机器上重现了这个问题,并且在Ubuntu服务器上也发生了同样的问题。 编辑2 我已经测试了jaxen作为javax.xml包的替代方案,但结果是相同的,这使我相信该错误深藏在sun webkit中。

可能相关:https://dev59.com/fmw15IYBdhLWcg3w4_tL - RDM
2
我可以在Windows 7 64位,Java 1.8.0_60上重现这个问题。它似乎是一个内存泄漏。我尝试在任意的XML文件上执行相同的循环而不涉及JavaFX,并得到了相同的结果。 - VGR
谢谢您关注这个问题!我甚至没有考虑过不使用JavaFX,但您是完全正确的,这个错误更深层次,提供w3c文档的方式并不重要。 - RDM
如果您想消除一些潜在的弱点,比如潜在的DOM序列化问题,您可以使用HTML Cleaner。在切换到这个工具之前,我曾经因为DOM序列化而遇到了相当快速的类加载和内存泄漏问题。使用Apache Async Http Client池时,每秒请求数约为70,但我也使用FX。 - Andrew Scott Evans
1个回答

7
我在Ubuntu中使用jdk1.8.60复制了泄漏。我进行了相当多的分析和调试,核心原因很简单,可以轻松地修复。XPath内容中没有内存泄漏。
有一个名为com.sun.webkit.Disposer的类,它正在持续清理一些在XPath评估期间创建的内部结构。Disposer通过Invoker.getInvoker().invokeOnEventThread(this);内部调用清理。如果您反编译代码,可以看到它。有不同的调用程序实现,使用不同的线程。如果您在JavaFX中工作,则Invoker会在JavaFX线程中定期执行清理。
然而,您的changed监听器方法也在JavaFX线程中调用,并且永远不会返回,因此清理永远没有机会发生。
我修改了您的代码,使changed方法仅生成一个新线程并返回,处理是异步完成的。猜猜 - 内存不再增长:
@Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
    if (newValue==Worker.State.SUCCEEDED) {
        new Thread(() ->{
            try {
                while(true){
                    NodeList eval = eval(engine.getDocument(), "//span[@class='title']");
                    int s = eval.getLength();
                }
            } catch (XPathExpressionException ex) {
                Logger.getLogger(MVC.class.getName()).log(Level.SEVERE, null, ex);
            }
        }).start();
    }
}

非常好的发现。我也能够在我的主要项目中应用这个原则(如上所述,问题中的代码只是一个MVC),现在内存不再增长了。在我的主要项目中,我没有做任何像永远阻塞事件队列这样愚蠢的事情 - 或者至少不是故意的,但显然我无意中这样做了(它是一个异步回调的大巢穴),但是简单地引入一个新线程,在最繁忙的地方释放事件线程以运行处理器,使内存保持稳定。谢谢和+rep。 - RDM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接