JUnit 4在Eclipse和Maven2中运行测试时,PermGen大小溢出

9
我正在使用JUnit、PowerMock和Mockito进行单元测试。我有很多测试类,用@RunWith(PowerMockRunner.class)@PrepareForTest(SomeClassesNames)注释来模拟最终类和200个以上的测试用例。
最近,在Eclipse或Maven2中运行整个测试套件时,我遇到了PermGen空间溢出的问题。当我逐个运行测试时,每个测试都成功了。
我做了一些研究,但是没有任何建议对我有帮助(我已经增加了PermGenSize和MaxPermSize)。最近我发现有一个只包含静态方法的类,每个方法都返回由PowerMockito模拟的对象。我想知道这是否是一个好的实践,也许这就是问题的根源,因为静态变量在单元测试之间被共享?
总的来说,是否有一个具有许多静态方法并返回静态模拟对象的静态类是一个好的实践?

你的应用程序为什么要设计这么多静态方法?静态方法往往会使测试变得困难。你能否将它们作为某个类的非静态方法,然后将该类的对象注入到需要它的任何代码中呢? - Dawood ibn Kareem
@DavidWallace,从OP所说的内容中,我认为静态方法用于测试中,它们仅返回模拟对象。它们不会在非测试代码中使用。 - Matthew Farwell
好的,我的理解与你不同。@Adam,请问能否澄清一下?这些静态方法是你的应用程序的一部分还是只是测试工具? - Dawood ibn Kareem
澄清一下,静态方法是测试脚手架的一部分。 - BlueLettuce16
为什么?你不能把它们变成非静态方法,放在它们所在的测试类中吗? - Dawood ibn Kareem
我已经改过了,但是并没有太大帮助 :( - BlueLettuce16
3个回答

28

我在Eclipse中也遇到了Junit中的PermGen错误。但是我没有使用任何模拟库,例如Mockito或EasyMock。然而,我的代码库很大,我的Junit测试使用Spring-Test(并且是复杂和强度大的测试用例)。因此,我需要为所有的Junit测试真正增加PermGen。

Eclipse将已安装的JRE设置应用于Junit运行 - 而不是eclipse.ini设置。因此,要更改这些设置:

  • Window > Preferences > Java > Installed JRE's
  • 选择默认的JRE,点击“编辑”按钮
  • 添加到默认VM参数:-XX:MaxPermSize=196m

这个设置将允许Junit测试在Eclipse中运行更强的测试用例,并避免OutOfMemoryError:PermGen。这也应该是低风险的,因为大多数简单的Junit测试不会分配所有这些内存。


9
正如@Brice所说,PermGen的问题来自于您对模拟对象的广泛使用。Powermock和Mockito都会创建一个新类,该类位于被模拟类和测试代码之间。该类在运行时创建并加载到PermGen中,并且(几乎)永远不会被回收。因此,您在PermGen空间上遇到了问题。
回答您的问题:
1)共享静态变量被认为是一种代码异味。在某些情况下是必要的,但它会在测试之间引入依赖关系。测试A需要在测试B之前运行。
2)使用静态方法返回模拟对象并不是真正的代码异味,这是一种常用的模式。如果您真的无法增加permgen空间,则有多种选择:
使用模拟池,在将模拟对象放回池中时使用PowerMock#reset()。这将减少您正在进行的创建数量。
其次,您说您的类是final的。如果可以更改,则可以在测试中使用匿名类。这再次减少了使用的permgen空间量:
Foo myMockObject = new Foo() {
     public int getBar() { throw new Exception(); }
}

第三,你可以引入一个接口(在Eclipse中使用重构 -> 提取接口),然后扩展一个什么也不做的空类。然后,在你的类中,你可以像上面那样做。我经常使用这种技术,因为我发现它更容易阅读:

public interface Foo {
  public int getBar();
}

public class MockFoo implements Foo {
  public int getBar() { return 0; }
}

接下来在类中:

Foo myMockObject = new MockFoo() {
     public int getBar() { throw new Exception(); }
}

我必须承认我并不是特别喜欢嘲笑,只有在必要的时候才会使用它,我倾向于使用匿名类扩展类或创建一个真正的MockXXX类。有关这个观点的更多信息,请参见Uncle Bob的《Mocking Mocking and Testing Outcomes》

顺便说一句,在maven surefire中,您总是可以使用forkMode=always,这将为每个测试类分叉jvm。但这并不能解决您的Eclipse问题。


我已将Surefire报告的PermGenSize从默认设置增加到128MB,并在Eclipse中对JUnit运行器进行了相同的操作 - 这有所帮助。这只是暂时的解决方案。我认为我的测试代码需要进行一些重构,因为我有大约250个测试用例,而要运行它们,我需要100MB的permgen内存。在我看来,这太多了。 - BlueLettuce16
尝试使用@PowerMockIgnore(value = {"org.apache.log4j.*"}) http://www.gitshah.com/2010/07/how-to-fix-outofmemoryerror-when.html - Alex Punnen
回答得好,还感谢一下 Uncle Bob 的帖子链接 :) - rottweiler

5

首先:Mockito使用CGLIB创建模拟对象,而PowerMock则使用Javassist进行其他操作,例如删除final标记,Powermock还会在新的ClassLoader中加载类。CGLIB因为吃掉Permanent Generation而闻名(只需在Google上搜索CGLIB PermGen即可找到相关结果)。

这不是一个直接的答案,因为它取决于您项目的细节:

  1. 正如您所指出的,存在静态辅助类,我不知道它是否也包含模拟的静态变量,我不知道您代码的细节,因此这只是纯猜测,其他更了解的读者可能会纠正我。

    这可能是由于加载此静态类的ClassLoader(至少其中一些子类)可能会在测试中保持活动状态 - 这可能是由于静态变量(存在于Class realm中)或某个地方的引用 - 这意味着如果ClassLoader仍然存活(即未垃圾回收),则其加载的类不会被丢弃,即生成的类仍然在PermGen中。

  2. 如果要加载大量这些类,则这些类的大小也可能很大,这可能与具有较高PermGen值有关,特别是因为Powermock需要为每个测试在新的Classloader中重新加载类。

再次说明,我不知道您项目的详细信息,因此我只是猜测,但您的永久代问题可能是由于第1点或第2点甚至两者都造成的。

总的来说,我会说是的:在这里拥有一个可能返回静态模拟对象的静态类看起来像是一种不好的做法,因为通常在生产代码中也是如此。如果制作不当,它会导致ClassLoader泄漏(这很讨厌!)。
实际上,我曾经看到运行数百个测试(仅使用Mockito),而不改变内存参数并且没有看到CGLIB代理被卸载,而且我没有使用Mockito API之外的静态内容。
如果您正在使用Sun/Oracle JVM,您可以尝试以下选项来跟踪发生的情况:
-XX:+TraceClassLoading和-XX:+TraceClassUnloading或-verbose:class
希望这有所帮助。

超出本问题的范围:

个人而言,我不喜欢使用Powermock,只在极端情况下使用,例如测试不可修改的遗留代码。在我看来,Powermock 太过入侵性,每次测试都必须生成新的类加载器来执行其操作(修改字节码),你必须大量注释测试类才能进行模拟等操作...... 在我看来,对于通常的开发来说,所有这些小不便之处都抵不过模拟final的好处。即使 Powermock 的作者 Johan 也曾告诉我,他建议使用 Mockito,并将 Powermock 保留给某些特定的用途。

请勿误解:Powermock 是一项非常棒的技术,在处理(糟糕的)设计不佳的遗留代码时真正有帮助。但对于日常开发,特别是实践 TDD,就不太适用了。


你所说的“巨大尺寸”是指测试类很大,还是被测试的类很大?我有超过250个测试用例,它们在permgen中使用了100MB。你认为这太多了吗?我只在极端情况下使用Powermock。感谢你的答复 - 跟踪选项对我很有帮助。 - BlueLettuce16
是的,这是CGLIB的一个问题,如果这些类没有被垃圾回收,那肯定是因为类加载器仍然被某个地方引用,可能是通过某个静态字段引用的其他类。无论如何,如果确实是类加载器泄漏,那就是一个麻烦的问题,很难找到并解决。此外,您可能需要跟踪多次加载的类,并且测量每个测试类的内存可能会很有趣。另外,您可能需要使用jmap / Eclipse MAT / Netbeans分析器等工具来实际分析内存。 - bric3
PowerMock是一项令人钦佩的技术,但它确实会因为大量使用自定义类加载而有时会引起问题。幸运的是,“无限制”模拟并不需要自定义类加载(顺便说一下,这也是CGLIB所做的)。JMockit通过修改已加载类的字节码而无需创建新类来实现。 (此外,它不需要对测试类进行注释。) - Rogério
@Rogerio 好的,但是如果我没记错的话,attach API 只在 Java 6 中可用(我记得 JBoss Byteman 的幻灯片上有提到)。我仍然知道许多项目仍然依赖于 JDK 5。无论如何,对于未来的工作来说,现在我们已经创建了一个扩展点来使用不同的引擎创建模拟对象,这真的是非常好的消息。 - bric3
@Brice 是的,使用Attach API需要JDK 1.6+。不过这并不是真正的问题,因为大多数用户已经在Java 6+上运行他们的测试,那些被困在JDK 1.5上的用户可以使用“-javaagent”参数。实际上,一些项目仍然部署到Java 5服务器,但这并不妨碍它们在开发/构建机器上安全地运行使用JDK 1.6甚至更新版本的测试 - 只需使用“javac -source 1.5 -target 1.5”。 - Rogério
显示剩余6条评论

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