2010-10-19 44 views
4

我运行了这些代码,并且遇到了一些问题,这有点奇怪。如果Java字符串是不可变的,并且StringBuilder是可变的,他们为什么会浪费我的代码中相同数量的内存?

使用字符串:

while(true) 
{ 
    String s = String.valueOf(System.currentTimeMillis()); 
    System.out.println(s); 
    Thread.sleep(10); 
} 

使用StringBuilder的:

StringBuilder s = null; 
    while(true) 
    { 
     s = new StringBuilder(); 
     s.append(System.currentTimeInMillis()); 
     System.out.println(s); 
     Thread.sleep(10); 
    } 

在他们被困在12540ķ浪费内存两种情况。在Windows XP SP2上运行此测试。

他们为什么浪费相同数量的内存? 为什么不可变的字符串停止浪费内存? 题外话:如何将StringBuilder转换为以特定字符集编码的字节数组?

+0

没有100%你是问。上述两者都不会导致任何“浪费”或泄漏的记忆。 12540k可能只是JVM使用的基本内存或还没有被清理的瞬态内存。 – 2010-10-19 11:04:56

+2

为什么不可变类会浪费内存? (而且可变吗?) – Ishtar 2010-10-19 11:08:16

+0

我读过常见的字符串,垃圾回收器从来没有收集过,是否是假的? – fredcrs 2010-10-19 11:12:04

回答

6

很难弄清楚你究竟在这里问什么,但应用程序的行为完全如我所料。

字符串是不可变的,垃圾收集器不会将它们取出。是不是

可变和不可变对象都可以在Java中进行垃圾收集。

实际判断一个物体是否实际上是垃圾收集的标准是可达性。简而言之,当垃圾收集器发现应用程序不能再使用某个对象时,该对象将被删除。

在这两个应用程序中,每10毫秒创建一个大小大致相同的对象。在每次迭代中,正在创建一个新对象,并将其引用分配给s,替换之前的引用。这使得以前的对象无法访问,并有资格进行垃圾回收。在某些时候,Java VM决定运行垃圾收集器。这将摆脱所有无法访问的对象...并且应用程序将继续。

我看过那些常见的字符串不是垃圾收集器收集的,是假的?

这体现在两个方面错误:

  • 字符串由new String(...)String.substring(...)创建等都是从其他Java对象没有什么不同。

  • 被拦截的字符串(通过调用String.intern())被存储在保存在PermGen堆中的字符串池中。然而,即使是PermGen堆也是垃圾回收,虽然时间更长,通常创建对象的堆也是如此。

(曾几何时,PermGen的堆被当作垃圾回收,但被前改变了很长的时间。)

3

因为你正在建造和投掷。事实上,你并不是真的使用StringBuilder来构建任何字符串。注意,你正在实例化一个新的StringBuilder对象。

+1

是的,但我使用currentTimeInMillis,它不应该是相同的 – fredcrs 2010-10-19 11:20:30

+0

是的正确。我在过去有不同的经历。你知道Java 1.4天。刚刚尝试过,看起来很好。谢谢。 – 2010-10-19 11:26:03

4
  1. 您似乎认为一个可变类会浪费更多的内存比不可变类。我不明白为什么。

  2. 你的代码是错误的,如果它打算在每个循环中分配更多的内存。它只是将s引用分配给一个新对象,因此前一个对象会丢失并最终被垃圾收集。

  3. 查看JVM的OS内存是对Java分配内存的非常粗略/不精确的估计。 (String和StringBuffer和char [])都是有效的,它们为每个字符分配大约2个字节(Java使用一些UTF-16变体)加上一个小的(可忽略大字符串)开销。

6

你是混淆了两个非常不同的东西:

  • 字符串是不可改变的,但是这无关他们是否是垃圾收集。但是,这意味着如果您需要对String进行大量更改(例如通过一次追加一个字符来构建一个大字符串),那么您最终会为垃圾收集器制作大量副本和大量工作。
  • 字符串文字(即直接写入源代码的字符串)是实体字符串池的一部分,通常不会垃圾收集。但是,这样做是为了允许源代码中同一个字符串的多个实例替换为对同一个对象的引用,这可以节省大量空间。这是唯一可能的,因为字符串是不可变的,所以程序的两个部分持有对同一个字符串的引用不会互相干扰。
+0

是的,我很困惑这些。谢谢 – fredcrs 2010-10-19 11:33:41

2

正如已经解释过的那样,因为你不是在改变字符串,而是只是指向一个新的值;旧的价值必须被垃圾收集。以下是使用stringBuffer尝试实际改变指向的值的代码片段。

StringBuffer s = new StringBuffer(); 
    while(true) 
    { 
     s.replace(0,13,Long.toString(System.currentTimeMillis())); 
     System.out.println(s); 
     Thread.sleep(10); 
    } 

应该指出,这并不能解决问题,因为两件事。首先,我们必须每次使用Long.toString()创建一个新的String,其次要调用s.toString()。这将使一个新的String分享stringBuffer的值(至少在我上次检查时就是这种情况)。当我们做s.replace它会分配一个新的数组来保存这个新的字符串,以保持字符串的不可变性。

其实,在这个简单的情况下,最好的,你可以做(​​据我所知)是:

while(true) 
    { 
     System.out.println(Long.toString(System.currentTimeMillis())); 
     Thread.sleep(10); 
    } 
+0

你给出的最后一个例子是最正确的一个例子,因为它没有使用中间字符串变量做任何shenanagins。 @fredcrs除非实际需要多次使用结果,否则不要将中间结果分配给变量。如果函数调用链看起来太长,则应该将某些嵌套调用重构为单独的函数,然后调用该函数。 – AJMansfield 2013-09-07 19:38:49

2

想后将此视为斯蒂芬℃的答复,但由于某些原因,我可以”吨;所以这里有一点澄清...

String.subString(...)不会创建一个新的字符串。它引用现有字符串中的一个点,并且返回子字符串值是向您的应用程序引入内存泄漏的一种可靠方法(尤其是在基于另一个字符串列表的子字符串值构建字符串列表时)。在这种情况下

最好的做法是:

return new String(s.subString(...)); 
+0

@Jeff Knecht - 你需要明白你的事实。 'String.substring()'创建一个新的'String'。当然,新的'String'与原始的'String'共享一些状态(即后备数组),但它仍然是一个新的'String'。其次,当然不是最好的做法,那就是'new String(s.subString(...)',只有在真正存在内存泄漏的风险时才应该这样做。在大多数情况下,不是。 – 2012-06-27 09:14:54

相关问题