gpt4 book ai didi

java - Java字符串垃圾回收:或者为什么这会消耗这么多内存

转载 作者:搜寻专家 更新时间:2023-11-01 03:48:06 25 4
gpt4 key购买 nike

解决了

我试图了解为什么我的单元测试之一会消耗这么多的内存。我所做的第一件事就是使用VisualVM运行该测试并进行测量:

enter image description here

最初的平坦线归因于测试开始时的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);
}

测试非常简单:重复执行10万次,以确保 defaultCRM.getReqClientID()生成具有有效计数器(介于000-999之间)的正确ReqClientID对象,并确保随机化前缀在翻转时正确更改。
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设置器,计算一个递增计数器,并在翻转时添加随机前缀。

假设我注释了上面编号为#1-#4的setter(关联的断言,以便它们不会失败)。内存使用现在是合理的:

enter image description here

最初,我是在setter组件中使用 +进行简单的字符串连接的。我改为 String.format(),但是没有任何效果。我也尝试了 StringBuilderappend()无效。

我还尝试了一些GC设置。特别是,我尝试了 -XX:+UseG1GC-XX:InitiatingHeapOccupancyPercent=35-Xms1g -Xmx1g(请注意,在我的构建从机上1g仍然不合理,我希望将其最大长度降至256m附近)。这是图形:

enter image description here

降至 -Xms25m -Xmx256m会导致OutOfMemoryError。

由于第三种原因,我对此行为感到困惑。首先,我不了解第一张图中 未使用的堆空间的极大增长。我创建一个对象,创建一些字符串,将字符串传递给该对象,并通过使其超出范围来删除该对象。显然,我不希望内存有完美的重用,但是为什么JVM似乎每次都为这些对象分配更多的堆空间?未使用的堆空间如此快地增长的方式看来确实非常错误。尤其是在使用更具侵略性的GC设置的情况下,我希望看到JVM在翻阅大量内存之前尝试回收这些完全未引用的对象。

其次,在图2中,很明显,实际的问题是字符串。我尝试阅读一些有关组成字符串,文字/实习生等方式的方法,但是除了 + / String.format() / StringBuilder之外,我看不到其他替代方法,它们似乎都产生相同的结果。我是否缺少一些神奇的方式来构建琴弦?

最后,我知道100K迭代是多余的,我可以用2K来测试过渡,但是我试图了解JVM中发生了什么。

系统:OpenJDK x86_64 1.8.0_92和热点x86_64 1.8.0_74。

编辑:

几个人建议在测试中手动调用 System.gc(),因此我尝试每隔1K循环执行一次。这对内存使用有显着影响,而对性能则有严重影响:

enter image description here

首先要注意的是,虽然使用的堆空间增长较慢,但仍然是 无限。唯一完全稳定的时间是循环结束,并调用结尾的 Thread.sleep()。几个问题:

1)为什么未使用的堆空间仍然很高?在第一个循环迭代期间,将调用 System.gc()( i % 1000 == 0)。实际上,这导致未使用的堆空间减少。为什么在第一次调用后总堆空间从未减少?

2)非常粗略地讲,每个循环迭代执行5个分配:inst ClientReqId和4个字符串。在每次循环迭代中,所有5个对象的所有引用都将被忘记。在整个 整个测试中,对象总数基本上保持静态(仅变化〜±5个对象)。我仍然不明白为什么当 Activity 对象的数量保持不变时 System.gc()在保持使用的堆空间不变方面没有更有效。

编辑2:解决了

@Jonathan通过询问 mockedDatawireConfig向我指出了正确的方向。这实际上是Spring @ConfigurationProperties类(即Spring将数据从Yaml加载到实例中并在需要的地方连接该实例)。在单元测试中,我没有使用任何与Spring相关的东西(单元测试,而不是集成测试)。在这种情况下,它只是带有getter和setter的POJO,但类中没有逻辑。

无论如何,单元测试使用的是它的模拟版本,您可以在上面的 setup()中看到它。我决定切换到对象的真实实例,而不是模拟对象。这就彻底解决了问题! Mockito似乎是固有的问题,或者可能是因为我似乎正在使用2.0.2- beta 。如果确实存在未知问题,我将作进一步调查,并与Mockito开发人员联系。

看一下dat甜美,甜美的图表:

enter image description here

最佳答案

好吧,这取决于JVM如何分配堆空间。它只是看到内存消耗大幅度(且快速!)增加,因此分配了足够的堆空间以免发生OutOfMemoryException。

您已经看到,可以通过使用参数来更改此行为。您还可以看到,一旦使用率保持不变,堆就不会进一步增长(它停止在〜3G而不是直到〜8G为止)。

要真正看到正在发生的事情,您不应该进行一些printf调试(这意味着注释掉某些东西并查看会发生什么),而应该使用IDE或其他工具来检查正在使用什么内存。

这样做将向您显示(例如):120k个String实例将2GiB或1.5GiB垃圾和500MiB用作字符串。
然后,您清楚地知道它是只是一个惰性集合(因为一个集合有开销)还是您仍然有一些引用在徘徊(我想说不,因为增长停止了)。

作为一种肮脏的解决方法,您还可以在循环中添加System.gc()调用以强制执行Garbage-Collection,以查看其是否可以提高堆使用率(当然会浪费CPU时间)。

关于java - Java字符串垃圾回收:或者为什么这会消耗这么多内存,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37843795/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com