解决
我试图理解为什么我的一个单元测试消耗了如此多的内存。我做的第一件事就是用VisualVM运行一次测试和测量:
初始扁平线是由测试开始时的Thread.sleep()
导致VisualVM有时间启动。
测试(和设置方法)非常简单:
@BeforeClass
private void setup() throws Exception {
mockedDatawireConfig = mock(DatawireConfig.class);
when(mockedDatawireConfig.getUrl()).thenReturn(new URL("http://example.domain.fake/"));
when(mockedDatawireConfig.getTid()).thenReturn("0000000");
when(mockedDatawireConfig.getMid()).thenReturn("0000000");
when(mockedDatawireConfig.getDid()).thenReturn("0000000");
when(mockedDatawireConfig.getAppName()).thenReturn("XXXXXXXXXXXXXXX");
when(mockedDatawireConfig.getNodeId()).thenReturn("t");
mockedVersionConfig = mock(VersionConfig.class);
when(mockedVersionConfig.getDatawireVersion()).thenReturn("000031");
defaultCRM = new ClientRefManager();
defaultCRM.setVersionConfig(mockedVersionConfig);
defaultCRM.setDatawireConfig(mockedDatawireConfig);
}
@Test
public void transactionCounterTest() throws Exception {
Thread.sleep(15000L);
String appInstanceID = "";
for (Long i = 0L; i < 100000L; i++) {
if (i % 1000 == 0) {
Assert.assertNotEquals(defaultCRM.getAppInstanceID(), appInstanceID);
appInstanceID = defaultCRM.getAppInstanceID();
}
ReqClientID r = defaultCRM.getReqClientID(); // This call is where memory use explodes.
Assert.assertEquals(getNum(r.getClientRef()), new Long(i % 1000));
Assert.assertEquals(r.getClientRef().length(), 14);
}
Thread.sleep(10000L);
}
测试非常简单:迭代100K次以确保defaultCRM.getReqClientID()
生成一个正确的ReqClientID对象,其有效计数器在000-999之间,并且随机化前缀在翻转时正确地更改。
defaultCRM.getReqClientID()
是发生内存问题的地方。我们来看看:
public ReqClientID getReqClientID() {
ReqClientID req = new ReqClientID();
req.setDID(datawireConfig.getDid()); // #1
req.setApp(String.format("%s&%s", datawireConfig.getAppName(), versionConfig.toString())); // #2
req.setAuth(String.format("%s|%s", datawireConfig.getMid(), datawireConfig.getTid())); // #3
Long c = counter.getAndIncrement();
String appID = appInstanceID;
if(c >= 999L) {
LOGGER.warn("Counter exceeds 3-digits. Resetting appInstanceID and counter.");
resetAppInstanceID();
counter.set(0L);
}
req.setClientRef(String.format("%s%s%03dV%s", datawireConfig.getNodeId(), appID, c, versionConfig.getDatawireVersion())); // #4
return req;
}
非常简单:创建一个对象,调用一些String
setter,计算递增计数器,以及rollover上的随机前缀。
让我说我注释掉了setter(相关联的断言,所以他们没有失败)编号为#1-#4。内存使用现在是合理的:
最初我在setter组件中使用+
使用简单的字符串连接。我改为String.format()
,但这没有任何效果。我也尝试StringBuilder
append()
无效。
我也尝试了一些GC设置。特别是,我尝试了-XX:+UseG1GC
,-XX:InitiatingHeapOccupancyPercent=35
和-Xms1g -Xmx1g
(请注意,在我的构建工具上,1g仍然是不合理的,我希望将其降低到最大256m左右)。这是图表:
向下-Xms25m -Xmx256m
会导致OutOfMemoryError。
由于第三个原因,我对此行为感到困惑。首先,我不理解第一个图中未使用的堆空间的极端增长。我创建一个对象,创建一些字符串,将字符串传递给对象,并通过让它超出范围来删除对象。显然,我不希望完全重用内存,但为什么JVM似乎每次都为这些对象分配更多的堆空间呢?未使用的堆空间增长如此快得多的方式似乎真的非常错误。特别是对于更积极的GC设置,我希望看到JVM尝试在翻阅内存之前回收这些完全未引用的对象。
其次,在图#2中,显然实际问题是字符串。我试图阅读有关组合字符串,文字/实习等方法的一些内容,但我看不到+
/ String.format()
/ StringBuilder
以外的许多替代方法,似乎产生了相同的结果。我错过了构建字符串的神奇方法吗?
最后,我知道100K迭代是落水的,我可以用2K测试翻转,但我试图了解JVM中发生了什么。
系统:OpenJDK x86_64 1.8.0_92以及Hotspot x86_64 1.8.0_74。
修改
有几个人建议在测试中手动调用System.gc()
,所以我尝试每1K循环执行一次。这会对内存使用产生明显影响,并对性能造成严重影响:
首先要注意的是,虽然使用的堆空间增长较慢,但仍然无界。它完全平稳的唯一一次是循环结束,并调用结束Thread.sleep()
。几个问题:
1)为什么未使用的堆空间仍然如此之高?在第一次循环迭代期间,调用System.gc()
(i % 1000 == 0
)。这实际上导致了未使用堆空间的减少。为什么第一次调用后总堆空间不会减少?
2)非常粗略地,执行每个循环迭代5次分配:inst ClientReqId和4个字符串。每次循环迭代都会忘记对所有5个对象的所有引用。在整个测试中,总对象基本上保持静态(仅变化〜±5个对象)。我仍然不明白为什么System.gc()
在活动对象的数量保持不变时保持使用的堆空间不变更有效。
编辑2:已解决
@Jonathan通过询问mockedDatawireConfig
向我指出了正确的方向。这实际上是一个Spring @ConfigurationProperties类(即Spring将数据从yaml加载到实例中,并将实例连接到需要它的位置)。在单元测试中,我没有使用任何与Spring相关的东西(单元测试,而不是集成测试)。在这种情况下,它只是一个带有getter和setter的POJO,但是类中没有逻辑。
无论如何,单元测试使用的是模拟版本,您可以在上面的setup()
中看到。我决定切换到对象的真实实例而不是模拟。这完全解决了这个问题!这似乎是Mockito可能存在的一些问题,或者可能是因为我似乎使用的是2.0.2- beta 。我会进一步调查并联系Mockito开发人员,如果它确实是一个未知的问题。
看看dat甜蜜的甜蜜图表:
答案 0 :(得分:0)
好吧,它是由JVM实现的,它是如何分配堆空间的。它只是看到内存消耗的巨大(并且快速!)增加,因此分配足够的堆空间而不会遇到OutOfMemoryException。
您已经看到,您可以通过玩弄参数来改变这种行为。你也看到,一旦使用量不变,堆就不会再增长(它停在~3G而不是增长直到~8G)。
要真正了解正在发生的事情,你不应该做一些printf调试(这意味着要注释一些内容,看看会发生什么),而是使用你的IDE或其他工具来检查你的内存使用情况
这样做会显示(例如):120k实例的String消耗2GiB或1.5GiB垃圾和500MiB作为字符串。
然后你清楚地知道它是否只是一个懒惰的集合(因为一个集合有一个开销)或者你有一些参考仍在飞来飞去(我说不,因为增长停止)。
作为一种肮脏的解决方法,您还可以向循环添加System.gc()
调用以强制执行垃圾收集,以查看它是否可以提高堆使用率(当然是以CPU时间为代价)。