Java';s字符串垃圾收集:或者为什么这会消耗这么多内存

Java';s字符串垃圾收集:或者为什么这会消耗这么多内存,java,string,garbage-collection,Java,String,Garbage Collection,已解决 我试图理解为什么我的一个单元测试占用了这么多内存。我做的第一件事就是用VisualVM运行一个测试和测量: 最初的扁平线是由于测试开始时出现了一个Thread.sleep(),为VisualVM提供了启动时间 测试(和设置方法)非常简单: @BeforeClass private void setup() throws Exception { mockedDatawireConfig = mock(DatawireConfig.class); when(mockedDa

已解决

我试图理解为什么我的一个单元测试占用了这么多内存。我做的第一件事就是用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);
}
非常简单:创建一个对象,调用一些
String
setter,计算递增计数器,以及滚动时的随机前缀

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

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

我还尝试了一些GC设置。特别是,我尝试了
-XX:+UseG1GC
-XX:initialingHeapOccupencyPercent=35
,以及
-Xms1g-Xmx1g
(请注意,1g在我的buildslave上仍然不合理,我希望将其最大值降低到256m左右)。下面是图表:

转到
-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循环一次。这对内存使用和性能都有很大影响:

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

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

2) 非常粗略地说,每个循环迭代执行5次分配:inst ClientReqId和4个字符串。在每次循环迭代中,对所有5个对象的所有引用都会被遗忘。在整个试验过程中,所有物体基本上保持静止(仅变化~±5个物体)。我仍然不明白,当活动对象的数量保持不变时,为什么
System.gc()
不能更有效地保持已用堆空间不变

编辑2:已解决

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

无论如何,单元测试使用的是模拟版本,您可以在上面的
setup()
中看到。我决定切换到对象的真实实例,而不是模拟。这完全解决了问题!Mockito似乎存在一些问题,可能是固有的,也可能是因为我使用的是2.0.2-测试版。我将对此进行进一步调查,如果确实出现未知问题,请联系Mockito开发人员

看看dat sweet,sweet图表:


那么,如何分配堆空间取决于JVM的实现。它只是看到了内存消耗的巨大(而且很快!)增长,因此分配了足够的堆空间以避免发生OutOfMemoryException

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


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

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

作为一种肮脏的解决方法,您还可以向循环中添加
System.gc()
调用以强制执行垃圾处理-
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;
}