Java垃圾收集器和内存的问题

时间:2014-02-27 22:03:57

标签: java memory garbage-collection magnolia

我对Java应用程序有一个非常奇怪的问题。

本质上它是一个使用玉兰(一个cms系统)的网页,在生产环境中有4个实例可用。有时CPU在java进程中达到100%。

所以,第一种方法是进行线程转储,并检查有问题的线程,我发现的是奇怪的:

"GC task thread#0 (ParallelGC)" prio=10 tid=0x000000000ce37800 nid=0x7dcb runnable 
"GC task thread#1 (ParallelGC)" prio=10 tid=0x000000000ce39000 nid=0x7dcc runnable 

好的,这很奇怪,我从来没有像这样的垃圾收集器有问题,所以接下来我们做的是激活JMX并使用jvisualvm检查机器:堆内存使用率非常高(95%) )。

天真的方法:增加内存,因此问题需要更多时间才能在重新启动的服务器上出现,结果显示内存增加(6 GB!)问题在重新启动后20小时出现,而其他内存较少的服务器(4GB!)已经运行了10天,这个问题还需要几天时间再次出现。此外,我尝试使用服务器失败的apache访问日志,并使用JMeter将请求重播到本地服务器,以重现错误......它也不起作用。

然后我更多地调查了日志以找到这个错误

info.magnolia.module.data.importer.ImportException: Error while importing with handler [brightcoveplaylist]:GC overhead limit exceeded
at info.magnolia.module.data.importer.ImportHandler.execute(ImportHandler.java:464)
at info.magnolia.module.data.commands.ImportCommand.execute(ImportCommand.java:83)
at info.magnolia.commands.MgnlCommand.executePooledOrSynchronized(MgnlCommand.java:174)
at info.magnolia.commands.MgnlCommand.execute(MgnlCommand.java:161)
at info.magnolia.module.scheduler.CommandJob.execute(CommandJob.java:91)
at org.quartz.core.JobRunShell.run(JobRunShell.java:216)
at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:549)
    Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

另一个例子

    Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.util.Arrays.copyOf(Arrays.java:2894)
    at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:117)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:407)
    at java.lang.StringBuilder.append(StringBuilder.java:136)
    at java.lang.StackTraceElement.toString(StackTraceElement.java:175)
    at java.lang.String.valueOf(String.java:2838)
    at java.lang.StringBuilder.append(StringBuilder.java:132)
    at java.lang.Throwable.printStackTrace(Throwable.java:529)
    at org.apache.log4j.DefaultThrowableRenderer.render(DefaultThrowableRenderer.java:60)
    at org.apache.log4j.spi.ThrowableInformation.getThrowableStrRep(ThrowableInformation.java:87)
    at org.apache.log4j.spi.LoggingEvent.getThrowableStrRep(LoggingEvent.java:413)
    at org.apache.log4j.AsyncAppender.append(AsyncAppender.java:162)
    at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251)
    at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66)
    at org.apache.log4j.Category.callAppenders(Category.java:206)
    at org.apache.log4j.Category.forcedLog(Category.java:391)
    at org.apache.log4j.Category.log(Category.java:856)
    at org.slf4j.impl.Log4jLoggerAdapter.error(Log4jLoggerAdapter.java:576)
    at info.magnolia.module.templatingkit.functions.STKTemplatingFunctions.getReferencedContent(STKTemplatingFunctions.java:417)
    at info.magnolia.module.templatingkit.templates.components.InternalLinkModel.getLinkNode(InternalLinkModel.java:90)
    at info.magnolia.module.templatingkit.templates.components.InternalLinkModel.getLink(InternalLinkModel.java:66)
    at sun.reflect.GeneratedMethodAccessor174.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:622)
    at freemarker.ext.beans.BeansWrapper.invokeMethod(BeansWrapper.java:866)
    at freemarker.ext.beans.BeanModel.invokeThroughDescriptor(BeanModel.java:277)
    at freemarker.ext.beans.BeanModel.get(BeanModel.java:184)
    at freemarker.core.Dot._getAsTemplateModel(Dot.java:76)
    at freemarker.core.Expression.getAsTemplateModel(Expression.java:89)
    at freemarker.core.BuiltIn$existsBI._getAsTemplateModel(BuiltIn.java:709)
    at freemarker.core.BuiltIn$existsBI.isTrue(BuiltIn.java:720)
    at freemarker.core.OrExpression.isTrue(OrExpression.java:68)

然后我发现such problem is due to the garbage collector using a ton of CPU but not able to free much memory

好的,所以MEMORY的问题在CPU中表现出来,所以如果内存使用问题解决了,那么CPU应该没关系,所以我拿了一个堆转换,不幸的是它太大了,无法打开它(文件是10GB),无论如何我运行服务器本地加载了一点点并且使用了一个堆转储,打开它后,我发现了一些有趣的东西:

有一些TON实例

AbstractReferenceMap$WeakRef  ==> Takes 21.6% of the memory, 9 million instances
AbstractReferenceMap$ReferenceEntry  ==> Takes 9.6% of the memory, 3 million instances

另外,我发现一个Map似乎被用作“缓存”(可怕但却是真的),问题是这样的地图是不同步的并且它在线程之间共享(是静态的),问题可能不仅是并发写入,而且还有缺乏同步的事实,不能保证线程A将看到线程B对地图所做的更改,但是,我无法弄清楚如何使用以下方法链接此可疑地图内存eclipse分析器,因为它不使用AbstracReferenceMap,它只是一个普通的HashMap。

不幸的是,我们不直接使用这些类(显然代码使用它们,但不是直接使用它们),所以我似乎走到了尽头。

我的问题是

  1. 我无法重现错误
  2. 我无法弄清楚内存泄漏到底在哪里(如果是这样的话)
  3. 有什么想法吗?

9 个答案:

答案 0 :(得分:5)

绝对应该删除'no-op'finalize()方法,因为它们可能会使任何GC性能问题变得更糟。但我怀疑你还有其他的内存泄漏问题。

建议:

  • 首先摆脱无用的finalize()方法。

  • 如果您有其他finalize()方法,请考虑删除它们。 (根据最终确定做事情通常是一个坏主意......)

  • 使用内存分析器尝试识别泄漏的对象以及导致泄漏的原因。有很多SO问题......以及其他有关在Java代码中查找泄漏的资源。例如:


现在针对您的特殊症状。

首先,抛出OutOfMemoryError s的地方可能无关紧要。

但是,您拥有大量AbstractReferenceMap$WeakRefAbstractReferenceMap$ReferenceEntry个对象的事实是一个字符串指示,表明您的应用程序或它正在使用的库正在进行大量的缓存......并且缓存与问题有关。 (AbstractReferenceMap类是Apache Commons Collections库的一部分。它是ReferenceMapReferenceIdentityMap的超类。)

您需要跟踪那些WeakRefReferenceEntry对象所属的地图对象(或对象)以及它们引用的(目标)对象。然后你需要找出创建它/它们的原因,并找出为什么没有清除条目以响应高内存需求。

  • 你是否对其他地方的目标对象有强烈的引用(这会阻止WeakRefs被破坏)?

  • 是否正在使用错误的地图以引起泄漏。 (仔细阅读javadocs ......)

  • 多个线程是否使用了地图而没有外部同步?这可能导致腐败,这可能表现为大量存储泄漏。


不幸的是,这些只是理论,可能还有其他因素导致这种情况。事实上,可以想象这根本不是内存泄漏。


最后,你观察到当堆更大时问题更严重。对我来说,这仍然与Reference /缓存相关的问题一致。

  • Reference个对象比常规引用更适合GC。

  • 当GC需要“打破”Reference时,会产生更多的工作;例如处理参考队列。

  • 即使发生这种情况,最早在下一个GC循环之前仍然无法收集生成的无法到达的对象。

所以我可以看到一个充满引用的6Gb堆如何显着增加GC中花费的时间百分比......与4Gb堆相比,这可能会导致“GC Overhead Limit”机制提前启动。

但我认为这是一个偶然的症状,而不是根本原因。

答案 1 :(得分:3)

由于调试困难,您需要找到一种方法来重现它。只有这样,您才能测试实验性变化并确定它们是否会使问题变得更好或更糟。在这种情况下,我会尝试编写快速创建&删除服务器连接,创建服务器连接并快速发送内存昂贵的请求等。

在重现之后,尝试减小堆大小以查看是否可以更快地重现它。但这样做是因为小堆可能没有达到“GC开销限制”,这意味着GC花费了过多的时间(某种程度上是98%)试图恢复内存。

对于内存泄漏,您需要确定代码在哪里累积对象的引用。例如。它是否构建了所有传入网络请求的映射? 网络搜索https://www.google.com/search?q=how+to+debug+java+memory+leaks显示了许多有关如何调试Java内存泄漏的有用文章,包括使用您正在使用的Eclipse Memory Analyzer等工具的提示。搜索特定错误消息https://www.google.com/search?q=GC+overhead+limit+exceeded也很有帮助。

无操作finalize()方法不应该导致这个问题,但它们可能会加剧它。 finalize()上的文档显示,使用finalize()方法强制GC 两次确定实例未被引用(在调用finalize()之前和之后)。

因此,一旦您可以重现该问题,请尝试删除这些无操作finalize()方法,并查看问题是否需要更长时间才能重现。

内存中有很多AbstractReferenceMap$WeakRef个实例,这很重要。 弱引用的要点是引用一个对象而不强制它留在内存中。 AbstractReferenceMap是一个Map,可以让键和/或值成为弱引用或软引用。 (软引用的目的是尝试将对象保留在内存中,但是当内存变低时让GC释放它。)无论如何,内存中所有那些WeakRef实例可能会加剧问题但是不应该不要将引用的Map键/值保留在内存中。他们指的是什么?还有什么指的是那些对象?

答案 2 :(得分:3)

尝试使用工具查找源代码中的泄漏,例如plumbr

答案 3 :(得分:2)

有许多可能性,或许你已经探索过其中一些。

这绝对是某种内存泄漏。

如果您的服务器具有用户会话,并且当用户处于非活动状态超过X分钟/小时时,您的用户会话未到期或处理不当,您将获得已用内存的累积。

如果您有一个或多个程序生成的某些地图,并且您没有清除旧/不需要的条目的地图,您可能会再次获得已用内存的累积。例如,我曾经考虑添加一个地图来跟踪进程线程,以便用户可以从每个线程获取信息,直到我的老板指出在任何时候都没有完成线程从地图中删除,所以如果用户保持记录在活跃的时候,他们会永远坚持这些线索。

您应该尝试在非生产服务器上进行负载测试,您可以在其中模拟大量用户对应用的正常使用情况。甚至可能甚至比平时更低限制服务器的内存。

祝你好运,记忆问题很难找到。

答案 4 :(得分:1)

你说你已经尝试过jvisualvm来检查机器。也许,再试一次,就像这样:

  • 这次查看“Sampler - > Memory”标签。

  • 它应该告诉你哪些(类型)对象占用的内存最多。

  • 然后找出通常创建和删除此类对象的位置。

答案 5 :(得分:1)

  • 很多时候,插入JVM的java代理可能会导致“奇怪”的错误。如果您正在运行任何代理(例如jrebel / liverebel,newrelic,jprofiler),请先尝试不运行它们。
  • 使用非标准参数(-XX)运行JVM时也会发生奇怪的事情;已知某些组合会引起问题;您当前使用的是哪些参数?
  • 内存泄漏也可以在Magnolia本身,你试过Google搜索“木兰泄漏”吗?您使用的是第三方木兰模块吗?如果可能,请尝试禁用/删除它们。

问题可能只与您的一部分有关您可以尝试通过“重播”登台/开发服务器上的访问日志来重现问题。

如果没有其他工作,如果是我,我会做以下事情: - 尝试在“空”Magnolia实例上复制问题(没有我的任何代码) - 尝试在“空”Magnolia实例上复制问题(没有第三方模块) - 尝试升级所有软件(玉兰,第三方模块,JVM) - 最后尝试使用YourKit运行生产站点并尝试找到泄漏

答案 6 :(得分:0)

我的猜测是你有自动导入运行,它会调用一些ImportHandler实例。该处理程序配置为备份它将要更新的所有节点(我认为这是默认选项),并且因为您的数据类型中可能有很多数据,并且因为所有这些都是在会话中完成的内存不足。尝试找出它是哪个导入作业并禁用它的备份。

HTH, 扬

答案 7 :(得分:0)

您的内存泄漏似乎来自您的阵列。垃圾收集器无法识别从阵列中删除的对象实例,因此不会收集以释放内存。我的建议是当你从数组中删除一个对象时,将前一个对象的位置分配给null,因此垃圾收集器可以意识到它是一个null对象,并将其删除。怀疑这将是你的确切问题,但知道这些事情总是好的,并检查这是否是你的问题。

当您需要删除/清理它时,将对象实例分配给null也是很好的。这是因为finalize()方法是粗略和邪恶的,有时不会被垃圾收集器调用。最好的解决方法是自己调用它(或其他类似的方法)。这样,您可以确保已成功执行垃圾清理。正如Joshua Bloch在他的书中所说:Effective Java,第2版,第7项,第27页:避免使用终结器。 “终结器是不可预测的,通常是危险的,通常是不必要的”。您可以看到here部分。

因为没有显示代码,所以我看不出这些方法是否有用,但仍然值得了解这些方法。希望这些技巧可以帮到你!

答案 8 :(得分:0)

如上所述,我会与Magnolia的开发者联系,但同时:

您收到此错误,因为GC在运行中收集不多

  

如果太多,并发收集器将抛出OutOfMemoryError   在垃圾收集中花费的时间:如果超过98%的   垃圾收集所花费的总时间少于堆的2%   恢复后,将抛出OutOfMemoryError。

由于您无法更改实施,我建议以较少运行的方式更改GC的配置,以便以这种方式失败的可能性更小。

这是一个示例配置,只是为了让你开始参数,你必须弄清楚你的最佳位置。 GC的日志可能对此有所帮助

我的VM参数如下: -Xms = 6G -Xmx = 6G -XX:MaxPermSize参数= 1G -XX:新尺寸= 2G -XX:MaxTenuringThreshold = 8 -XX:SurvivorRatio = 7 -XX:+ UseConcMarkSweepGC -XX:+ CMSClassUnloadingEnabled -XX:+ CMSPermGenSweepingEnabled -XX:CMSInitiatingOccupancyFraction = 60 -XX:+ HeapDumpOnOutOfMemoryError -XX:+ PrintGCDetails -XX:+ PrintGCTimeStamps -XX:+ PrintTenuringDistribution -Xloggc:原木/ gc.log