使用标准Java库(1.6.0_27)评估XPath表达式时,似乎存在内存泄漏。

请参阅下面的代码来重现此问题:

public class XpathTest {

    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setNamespaceAware(true);
        DocumentBuilder builder = docFactory.newDocumentBuilder();
        Document doc = builder.parse("test.xml");

        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        XPathExpression expr = xpath.compile("//Product");

        Object result = expr.evaluate(doc, XPathConstants.NODESET);
        NodeList nodes = (NodeList) result;
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            System.out.println(node.getAttributes().getNamedItem("id"));

            XPathExpression testExpr = xpath.compile("Test");
            Object testResult = testExpr.evaluate(node, XPathConstants.NODE);
            Node test = (Node) testResult;
            System.out.println(test.getTextContent());
        }
        System.out.println(nodes.getLength());
    }
}


下面是一个示例XML文件:

<Products>
  <Product id='ID0'>
    <Test>0</Test>
  </Product>
  <Product id='ID1'>
    <Test>1</Test>
  </Product>
  <Product id='ID2'>
    <Test>2</Test>
  </Product>
  <Product id='ID3'>
    <Test>3</Test>
  </Product>
  ...
</Products>


当我使用NetBeans探查器运行此示例时,即使在垃圾回收之后,com.sun.org.apache.xpath.internal.objects.XObject类的分配似乎仍在增加。

我使用XPath库的方式有误吗?这是Java库中的错误吗?有潜在的解决方法吗?

最佳答案

在这种情况下,没有“内存泄漏”。内存泄漏定义为应用程序无法回收内存的实例。在这种情况下,不会泄漏,因为所有XObject(和XObject[])实例都可以在某个时间点回收。

从VisualVM获得的内存探查器快照得出以下观察结果:


调用XObject方法时,将创建所有XObject[](和XPathExpression.evaluate)实例。
当无法从GC根目录访问XObject实例时,将对其进行回收。在您的情况下,GC根是resulttestResult局部变量,它们在主线程堆栈中是局部的。


基于上述情况,我认为您的应用程序正在经历或可能会经历内存耗尽而不是内存泄漏。当您有大量来自XPath表达式求值的XObject / XObject[]实例,而这些实例尚未被垃圾回收器回收时,这是正确的。


它们仍然可以从GC根目录访问,
或垃圾收集器还没有来回收它们。


第一种解决方案的唯一解决方案是在所需的时间内将对象保留在内存中。您似乎并没有在代码中违反它,但是您的代码肯定可以提高效率-您保留第一个XPath表达式的结果,然后肯定可以更高效地执行第二个表达式。 //Product/Test可用于检索Test节点,并获取父级Product节点的id值,如以下代码段所示(该代码仅计算一个XPath表达式,而不是两个):

expr = xpath.compile("//Product/Test");
nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++)
{
    Node node = nodes.item(i);
    System.out.println(node.getParentNode().getAttributes().getNamedItem("id"));
    System.out.println(node.getTextContent());
}
System.out.println(nodes.getLength());


就第二个观察而言,您应该获取GC日志(使用verbose:gc JVM启动标志)。然后,如果创建的对象太短,则可以决定调整年轻一代的大小,因为可能的情况是可访问对象将移至有生命的一代,从而导致需要大型集合来回收对象本质上是短暂的。在理想的情况下(考虑您发布的代码),应在for循环的每几次迭代中执行一次年轻的gen收集周期,因为该循环局部的XObject实例应在该块的局部变量后立即回收超出范围。

09-05 06:09