Java垃圾收集器和内存问题

9
我遇到了一个与Java应用程序有关的非常奇怪的问题。
基本上,这是一个使用Magnolia(一种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)

后来我发现,这类问题是由于垃圾回收占用大量CPU而无法释放大量内存所导致的

好的,所以这是一个与内存有关的问题,表现在CPU上,因此,如果解决了内存使用问题,那么CPU就应该没问题了,所以我进行了一次堆转储,但不幸的是,文件太大了,无法打开(文件大小为10GB),不过我在本地运行服务器,加载了一些内容并进行了一次堆转储,打开后,我发现了一些有趣的东西:

有大量实例:

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

此外,我发现了一个似乎被用作“缓存”的Map(可怕但却是真的),问题在于这样的Map没有同步,并且它是在线程之间共享的(静态的)。问题不仅可能是并发写入,而且还可能是由于缺乏同步,线程A无法看到线程B对Map所做的更改,然而,我无法找出如何使用内存Eclipse分析器链接这个可疑的Map,因为它并没有使用AbstracReferenceMap,而只是普通的HashMap。
不幸的是,我们不直接使用这些类(显然代码使用它们,但不是直接使用),所以我似乎陷入了死胡同。
对我来说存在以下问题:
  1. 我无法复现错误
  2. 我无法弄清楚内存泄漏(如果有的话)到底出现在哪里
有任何想法吗?

3
我不熟悉Magnolia CMS,但这似乎是不正确使用apache common的ReferenceMap。可能有一些对象被“缓存”,使用ReferenceMap来加快查找速度,但仍允许GC在内存不足时删除条目。然而,如果在ReferenceMap中使用错误类型的引用(强引用),则不能保证GC可以删除这些条目。这将导致本来应该是短暂数据的长时间保留。值得联系Magnolia社区,很有可能其他人也曾经见过这个问题。 - ProgrammerDan
当前的Java版本为$ java -version java版本为"1.6.0_30" OpenJDK运行时环境(IcedTea6 1.13.1)(rhel-4.1.13.1.el5_10-x86_64) OpenJDK 64位服务器虚拟机(版本23.25-b01,混合模式) - Juan Antonio Gomez Moriano
@jalf 问题已更改,感谢您的建议。 - Juan Antonio Gomez Moriano
1
你尝试过创建堆转储并使用 https://www.eclipse.org/mat/ 等工具进行分析,以查看是什么一直占用着你的内存吗? - user2046264
MAT需要的内存几乎和它所分析的进程一样多。一旦你达到了20G左右,运行起来就非常困难了... - G. Blake Meike
显示剩余2条评论
9个回答

6
"“无操作”finalize()方法应该被删除,因为它们可能会使任何GC性能问题更糟。但我怀疑您还有其他内存泄漏问题。

建议:

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

  • 如果您有其他finalize()方法,请考虑摆脱它们。(依赖最终化来完成任务通常是不好的想法...)

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

"
现在针对您提到的症状。首先,OutOfMemoryError错误被抛出的位置可能不相关。然而,您拥有大量的AbstractReferenceMap $ WeakRefAbstractReferenceMap $ ReferenceEntry对象表明,您的应用程序或其使用的库正在进行大量缓存操作...并且这种缓存与问题有关。 (AbstractReferenceMap类是Apache Commons Collections库的一部分。它是ReferenceMapReferenceIdentityMap的超类。)您需要跟踪那些WeakRefReferenceEntry对象所属的映射对象(或对象),以及它们所引用的目标对象。然后,您需要找出是什么创建了它/它们,并弄清楚为什么条目没有响应高内存需求而被清除。
  • 您是否在其他地方拥有指向目标对象的强引用(这将防止弱引用被破坏)?

  • 地图是否被错误使用以导致泄漏。(仔细阅读javadoc...)

  • 地图是否在多个线程中使用而没有进行外部同步?这可能会导致数据损坏,从而潜在地表现为大量存储泄漏。


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


最后,你的观察是当堆更大时问题更严重。对我来说,这仍然与Reference/缓存相关问题一致。
  • Reference对象比常规引用对GC更具挑战性。

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

  • 即使发生这种情况,由此产生的不可达对象也无法在最早的下一个GC周期之前被收集。

因此,我可以看到一个6GB堆充满了References将显着增加在GC中花费时间的百分比...相比于4GB堆,这可能导致“GC Overhead Limit”机制更早地启动。

但我认为这只是偶然症状而不是根本原因。


我会在今天结束前再次阅读并更加具体。谢谢! - Juan Antonio Gomez Moriano
@JuanAntonioGomezMoriano,可能不是Map引起了问题。如果它确实对对象有弱引用,那么关键问题是这些对象是什么,以及对它们的强引用在哪里? - Jerry101
@Jerry101 - 同意。所以他需要做的第一件事是看看这些引用指的是什么。 - Stephen C
我同意弱引用似乎不太可能有兴趣。它们所指向的东西将在GC运行时被删除。然而,在我们放弃之前,是否有可能弱引用本身永远不会被清除或重用?也许,一旦WeakRef对象成为映射中的值,它就会永远留在映射中?如果映射键是可变的,那么这种情况可能发生... - G. Blake Meike
@G.BlakeMeike - 有很多原因可能导致弱引用没有被破坏。而且也有可能问题在于有太多的弱引用,处理它们会超过GC Overhead限制。请记住,这里的问题不一定是我们没有足够的内存。 - Stephen C
显示剩余3条评论

3
有一个棘手的调试问题,你需要找到一种重现它的方法。只有这样,你才能测试实验性的更改,并确定它们是否会使问题变得更好或更糟。在这种情况下,我会尝试编写循环,快速创建和删除服务器连接,创建服务器连接并快速发送占用内存的请求等。
在你能够重现它之后,尝试减小堆大小,看看是否可以更快地重现它。但是请先执行第二步,因为小堆可能不会触发“GC overhead limit”,这意味着GC花费过多时间(按某些标准达到98%)来恢复内存。
对于内存泄漏,你需要找出代码中积累对象引用的位置。例如,它是否构建了所有传入网络请求的Map?搜索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())显示,拥有一个finalize()方法会强制GC 两次确定实例未被引用(在调用finalize()之前和之后)。
因此,一旦你能够重现问题,请尝试删除那些无操作的finalize()方法,并查看问题是否需要更长时间才能重现。
重要的是,内存中有许多AbstractReferenceMap$WeakRef实例。弱引用的目的是引用对象而不强制其保留在内存中。 AbstractReferenceMap是一个Map,它允许将键和/或值设置为弱引用或软引用。(软引用的目的是尝试保留对象在内存中,但在内存不足时让GC释放它。)无论如何,在内存中的所有这些WeakRef实例可能会加剧问题,但不应该保留引用的Map键/值在内存中。它们指的是什么?还有什么东西在引用这些对象?

1
引用+1:“在面对一个棘手的调试问题时,你需要找到一种复现它的方法。只有这样,你才能测试实验性的更改,并确定它们是否会使问题变得更好或更糟。” - Sascha Wedler

3

试用一款工具,如plumbr,定位源代码中的漏洞。


2

有很多可能性,也许你已经探索了其中一些。

这肯定是某种内存泄漏。

如果您的服务器具有用户会话,并且在用户不活动超过X分钟/小时时您的用户会话没有过期或未被正确处理,则会导致使用的内存增加。

如果您的程序生成了一个或多个映射,并且您没有清除旧/不需要的条目,则可能再次增加使用的内存。例如,我曾考虑添加一个映射以跟踪进程线程,以便用户可以从每个线程获取信息,直到我的老板指出,完成的线程从未从映射中删除,因此如果用户保持登录并处于活动状态,则他们将永远占用这些线程。

您应该尝试在非生产服务器上进行负载测试,通过大量用户模拟您的应用程序的正常使用。甚至可以将服务器的内存限制得比平常更低。

祝好运,内存问题很难追踪。


谢谢你的提示,目前我们不跟踪用户会话,事实上你甚至无法登录页面,但我找到了一个像缓存一样工作的Map(呃),同时我发现finalize()被覆盖了,请你检查一下我的问题更新好吗? - Juan Antonio Gomez Moriano
如果 finalize 被重写为仅调用 super.finalize(),那么它与未被重写时并没有任何不同。 - Martin

1

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

  • 这次看看“取样器 -> 内存”选项卡。

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

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


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

问题可能与您的某个部分有关,您可以尝试通过在staging / development服务器上“重放”访问日志来复制问题。

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


0
我的猜测是你正在运行自动导入,它会调用某个ImportHandler实例。该处理程序被配置为备份所有要更新的节点(我认为这是默认选项),由于你的数据类型可能有很多数据,并且由于所有这些都在会话中完成,所以你会耗尽内存。尝试找出是哪个导入作业并为其禁用备份。
希望对你有所帮助, Jan

0

看起来你的内存泄漏是源自于你的数组。垃圾回收器很难识别从数组中删除的对象实例,因此不会被释放以释放内存。我的建议是当你从一个数组中删除一个对象时,将前一个对象的位置赋值为null,这样垃圾回收器就可以意识到它是一个null对象,并将其删除。我怀疑这可能不是你确切的问题,但了解这些事情并检查是否是你的问题总是好的。

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

由于没有显示任何代码,我无法确定这些方法是否有用,但了解这些内容仍然是值得的。希望这些提示能对您有所帮助!


0

如上所建议,我会与Magnolia开发人员联系,但同时:

您收到此错误是因为垃圾回收器在一次运行中没有进行很多回收

如果垃圾回收所占用的时间超过了总时间的98%且堆恢复不足2%,并发收集器将引发OutOfMemoryError。

由于您无法更改实现方式,我建议更改垃圾回收器的配置,以更少的频率运行,这样就不太可能以这种方式失败。

这里是一个示例配置,只是为了让您开始了解参数,您需要找到适合自己的最佳值。 GC的日志可能对此有帮助

我的虚拟机参数如下: -Xms=6G -Xmx=6G -XX:MaxPermSize=1G -XX:NewSize=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:logs/gc.log


或者,如果您必须运行不可更改的代码,请执行相反的操作:检测(内存不足)错误,然后自动重新启动服务器(如果可能)。 - Sascha Wedler

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