Java的字符串垃圾收集:或者为什么消耗这么多内存
解决了
我试图理解为什么我的一个单元测试消耗了这么多内存.我做的第一件事就是用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(),但是没有任何效果.我也尝试过使用append()的StringBuilder无效. 我也尝试了一些GC设置.特别是,我试过-XX:UseG1GC,-XX:InitiatingHeapOccupancyPercent = 35,和-Xms1g -Xmx1g(注意1g在我的buildlave上仍然是不合理的,我想让它在最大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甜蜜的甜蜜图: 解决方法
好吧,这取决于JVM如何分配堆空间的实现.它只是看到内存消耗的巨大(并且快速!)增加,因此分配足够的堆空间而不会遇到OutOfMemoryException.
您已经看到,您可以通过玩弄参数来改变这种行为.你也看到,一旦使用量不变,堆就不会再增长(它停在~3G而不是增长直到~8G). 要真正了解正在发生的事情,您不应该进行一些printf调试(这意味着要注释一些内容并查看会发生什么),而是使用您的IDE或其他工具来检查正在使用内存的内容. 这样做会显示(例如):120k实例的String消耗2GiB或1.5GiB垃圾和500MiB作为字符串. 作为一种肮脏的解决方法,您还可以向循环中添加System.gc()调用以强制执行垃圾收集,以查看它是否可以提高堆使用率(当然是以CPU时间为代价). (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |