Java 8 Update 20 的新特性 —— 字符串去重 已翻译 100%

oschina 投递于 2014/09/02 07:33 (共 5 段, 翻译完成于 09-04)
阅读 6677
收藏 59
1
加载中

字符串在任何应用中都占用了大量的内存。尤其数包含独立UTF-16字符的char[]数组对JVM内存的消耗贡献最多——因为每个字符占用2位。

内存的30%被字符串消耗其实是很常见的,不仅是因为字符串是与我们互动的最好的格式,而且是由于流行的HTTP API使用了大量的字符串。使用Java 8 Update 20,我们现在可以接触到一个新特性,叫做字符串去重,该特性需要G1垃圾回收器,该垃圾回收器默认是被关闭的。

字符串去重利用了字符串内部实际是char数组,并且是final的特性,所以JVM可以任意的操纵他们。

0x0bject
0x0bject
翻译于 2014/09/02 10:07
2

对于字符串去重,开发者考虑了大量的策略,但最终的实现采用了下面的方式:

无论何时垃圾回收器访问了String对象,它会对char数组进行一个标记。它获取char数组的hash value并把它和一个对数组的弱引用存在一起。只要垃圾回收器发现另一个字符串,而这个字符串和char数组具有相同的hash code,那么就会对两者进行一个字符一个字符的比对。

如果他们恰好匹配,那么一个字符串就会被修改,指向第二个字符串的char数组。第一个char数组就不再被引用,也就可以被回收了。

这整个过程当然带来了一些开销,但是被很紧实的上限控制了。例如,如果一个字符未发现有重复,那么一段时间之内,它会不再被检查。

0x0bject
0x0bject
翻译于 2014/09/02 10:20
2

那么该特性实际上是怎么工作的呢?首先,你需要刚刚发布的Java 8 Update 20,然后按照这个配置: -Xmx256m -XX:+UseG1GC 去运行下列的代码:

public class LotsOfStrings {
 
  private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>();
 
  public static void main(String[] args) throws Exception {
    int iteration = 0;
    while (true) {
      for (int i = 0; i < 100; i++) {
        for (int j = 0; j < 1000; j++) {
          LOTS_OF_STRINGS.add(new String("String " + j));
        }
      }
      iteration++;
      System.out.println("Survived Iteration: " + iteration);
      Thread.sleep(100);
    }
  }
}

这段代码会执行30个迭代之后报OutOfMemoryError。

现在,开启字符串去重,使用如下配置去跑上述代码:

-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics

此时它已经可以运行更长的时间,而且在50个迭代之后才终止。

JVM现在同样打印出了它做了什么,让我们一起看一下:

[GC concurrent-string-deduplication, 4658.2K->0.0B(4658.2K), avg 99.6%, 0.0165023 secs]
   [Last Exec: 0.0165023 secs, Idle: 0.0953764 secs, Blocked: 0/0.0000000 secs]
      [Inspected:          119538]
         [Skipped:              0(  0.0%)]
         [Hashed:          119538(100.0%)]
         [Known:                0(  0.0%)]
         [New:             119538(100.0%)   4658.2K]
      [Deduplicated:       119538(100.0%)   4658.2K(100.0%)]
         [Young:              372(  0.3%)     14.5K(  0.3%)]
         [Old:             119166( 99.7%)   4643.8K( 99.7%)]
   [Total Exec: 4/0.0802259 secs, Idle: 4/0.6491928 secs, Blocked: 0/0.0000000 secs]
      [Inspected:          557503]
         [Skipped:              0(  0.0%)]
         [Hashed:          556191( 99.8%)]
         [Known:              903(  0.2%)]
         [New:             556600( 99.8%)     21.2M]
      [Deduplicated:       554727( 99.7%)     21.1M( 99.6%)]
         [Young:             1101(  0.2%)     43.0K(  0.2%)]
         [Old:             553626( 99.8%)     21.1M( 99.8%)]
   [Table]
      [Memory Usage: 81.1K]
      [Size: 2048, Min: 1024, Max: 16777216]
      [Entries: 2776, Load: 135.5%, Cached: 0, Added: 2776, Removed: 0]
      [Resize Count: 1, Shrink Threshold: 1365(66.7%), Grow Threshold: 4096(200.0%)]
      [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
      [Age Threshold: 3]
   [Queue]
      [Dropped: 0]

为了方便,我们不需要自己去计算所有数据的加和,使用方便的总计就可以了。

上面的代码段规定执行了字符串去重,花了16ms的时间,查看了约 120 k 字符串。

0x0bject
0x0bject
翻译于 2014/09/02 10:31
2

上面的特性是刚推出的,意味着可能并没有被全面的审视。具体的数据在实际的应用中可能看起来有差别,尤其是那些应用中字符串被多次使用和传递,因此一些字符串可能被跳过或者早就有了hashcode(正如你可能知道的那样,一个String的hash code是被懒加载的)。

在上述的案例中,所有的字符串都被去重了,在内存中移除了4.5MB的数据。

[Table]部分给出了有关内部跟踪表的统计信息,[Queue]则列出了有多少对去重的请求由于负载被丢弃,这也是开销减少机制中的一部分。

那么,字符串去重和字符串驻留相比又有什么差别呢?我博客上有一篇文章,名叫how great String Interning is for memory efficiency 。事实上,字符串去重和驻留看起来差不多,除了暂留的机制重用了整个字符串实例,而不仅仅是字符数组。

0x0bject
0x0bject
翻译于 2014/09/02 11:45
2

JDK Enhancement Proposal 192的创造者的争论点在于开发者们常常不知道将驻留字符串放在哪里合适,或者是合适的地方被框架所隐藏.就像我写的那样,当碰到复制字符串(像国家名字)的时候,你需要一些常识.字符串去重,对于在同一个JVM中的应用程序的字符串复制也有好处,同样包括像XML Schemas,urls以及jar名字等一般认为不会出现多次的字符串.

当字符串驻留发生在应用程序线程中的时候,垃圾回收异步并发处理时,字符串去重也不会增加运行时的消耗.这也解释了,为什么我们会在上面的代码中发现Thread.sleep().如果没有sleep会给GC增加太多的压力,这样字符串去重根本就不会发生.但是,这只是示例代码才会出现的问题.实际的应用程序,常常会在运行字符串去重的时候使用几毫秒的时间.

gones945
gones945
翻译于 2014/09/03 23:21
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(23)

白小衣
白小衣

引用来自“白小衣”的评论

G1 现在成熟了吗?哪个jdk版本开始发展到比较成熟可以在生产环境中使用的呢?

引用来自“Raynor1”的评论

g1现在用的不是很多了啊。。但是还是要看你的业务场景的。。并不是所有的场景都适合的。。在JAVA 7里面就已经放里面了。。可以直接拿来用了。。
G1是jdk6还是7才有的,刚开始不是特别稳定,所以才没人用,所以我想问问现在成熟了没
Raynor1
Raynor1

引用来自“白小衣”的评论

G1 现在成熟了吗?哪个jdk版本开始发展到比较成熟可以在生产环境中使用的呢?
g1现在用的不是很多了啊。。但是还是要看你的业务场景的。。并不是所有的场景都适合的。。在JAVA 7里面就已经放里面了。。可以直接拿来用了。。
Smile月光
Smile月光
GavinTop
GavinTop
帆帆同学翻译的不错
其实快播挺纯洁
其实快播挺纯洁

引用来自“JacarriChan”的评论

new String("12345")与"12345"两者操作后最终结果所占用的内存一样了?

引用来自“李桂玺”的评论

据我了解,好像之前的jdk版本也是一样的。

引用来自“JacarriChan”的评论

String s1 = new String("123456"); String s2 = "123456"; System.out.println(s1 == s2); 在jdk1.6.0_37上执行的结果是false

引用来自“李桂玺”的评论

试过了,在jdk1.7.0_67的结果也是false,看来得多实践啊。

引用来自“彭小龙”的评论

这样测试用换都会返回false的,自己看看String的源码,正确的测试用例是: public static void main(String[] args) throws Exception { String str1 = new String("123".getBytes()); String str2 = new String("123".getBytes()); System.out.println(getStringValue(str1) == getStringValue(str2)); System.gc(); Thread.sleep(1000); System.out.println(getStringValue(str1) == getStringValue(str2)); } static char[] getStringValue(String str) throws Exception { Field field = String.class.getDeclaredField("value"); field.setAccessible(true); return (char[])field.get(str); }

引用来自“彭小龙”的评论

应该是去比较String的value属性(char[]),而不是String对象本身, 并且初始化的方式要注意,应该用构造方法public String(byte bytes[])

引用来自“李桂玺”的评论

我们讨论的是垃圾回收,如果两个字符串的value相同,那么他们应该有相同的地址,即str1==str2为true,文章提到的java8新特性应该是这个意思。
我看得懂文章,我测试也是测gc,甭管文章,这个特性实际说的是gc会回收对应的重复的char数组(String的value属性),而不是对应的String对象
其实快播挺纯洁
其实快播挺纯洁
我看得懂文章,我测试也是测gc,甭管文章,这个特性实际说的是gc会回收对应的重复的char数组(String的value属性),而不是对应的String对象
一野夏伊娃
一野夏伊娃

引用来自“JacarriChan”的评论

new String("12345")与"12345"两者操作后最终结果所占用的内存一样了?

引用来自“李桂玺”的评论

据我了解,好像之前的jdk版本也是一样的。

引用来自“JacarriChan”的评论

String s1 = new String("123456"); String s2 = "123456"; System.out.println(s1 == s2); 在jdk1.6.0_37上执行的结果是false

引用来自“李桂玺”的评论

试过了,在jdk1.7.0_67的结果也是false,看来得多实践啊。

引用来自“彭小龙”的评论

这样测试用换都会返回false的,自己看看String的源码,正确的测试用例是: public static void main(String[] args) throws Exception { String str1 = new String("123".getBytes()); String str2 = new String("123".getBytes()); System.out.println(getStringValue(str1) == getStringValue(str2)); System.gc(); Thread.sleep(1000); System.out.println(getStringValue(str1) == getStringValue(str2)); } static char[] getStringValue(String str) throws Exception { Field field = String.class.getDeclaredField("value"); field.setAccessible(true); return (char[])field.get(str); }

引用来自“彭小龙”的评论

应该是去比较String的value属性(char[]),而不是String对象本身, 并且初始化的方式要注意,应该用构造方法public String(byte bytes[])
我们讨论的是垃圾回收,如果两个字符串的value相同,那么他们应该有相同的地址,即str1==str2为true,文章提到的java8新特性应该是这个意思。
其实快播挺纯洁
其实快播挺纯洁

引用来自“JacarriChan”的评论

new String("12345")与"12345"两者操作后最终结果所占用的内存一样了?

引用来自“李桂玺”的评论

据我了解,好像之前的jdk版本也是一样的。

引用来自“JacarriChan”的评论

String s1 = new String("123456"); String s2 = "123456"; System.out.println(s1 == s2); 在jdk1.6.0_37上执行的结果是false

引用来自“李桂玺”的评论

试过了,在jdk1.7.0_67的结果也是false,看来得多实践啊。

引用来自“彭小龙”的评论

这样测试用换都会返回false的,自己看看String的源码,正确的测试用例是: public static void main(String[] args) throws Exception { String str1 = new String("123".getBytes()); String str2 = new String("123".getBytes()); System.out.println(getStringValue(str1) == getStringValue(str2)); System.gc(); Thread.sleep(1000); System.out.println(getStringValue(str1) == getStringValue(str2)); } static char[] getStringValue(String str) throws Exception { Field field = String.class.getDeclaredField("value"); field.setAccessible(true); return (char[])field.get(str); }
应该是去比较String的value属性(char[]),而不是String对象本身, 并且初始化的方式要注意,应该用构造方法public String(byte bytes[])
其实快播挺纯洁
其实快播挺纯洁

引用来自“JacarriChan”的评论

new String("12345")与"12345"两者操作后最终结果所占用的内存一样了?

引用来自“李桂玺”的评论

据我了解,好像之前的jdk版本也是一样的。

引用来自“JacarriChan”的评论

String s1 = new String("123456"); String s2 = "123456"; System.out.println(s1 == s2); 在jdk1.6.0_37上执行的结果是false

引用来自“李桂玺”的评论

试过了,在jdk1.7.0_67的结果也是false,看来得多实践啊。
这样测试用换都会返回false的,自己看看String的源码,正确的测试用例是: public static void main(String[] args) throws Exception { String str1 = new String("123".getBytes()); String str2 = new String("123".getBytes()); System.out.println(getStringValue(str1) == getStringValue(str2)); System.gc(); Thread.sleep(1000); System.out.println(getStringValue(str1) == getStringValue(str2)); } static char[] getStringValue(String str) throws Exception { Field field = String.class.getDeclaredField("value"); field.setAccessible(true); return (char[])field.get(str); }
Y
YnCnWi
String s1 = new String("123456"); String s2 = "123456";
我可不可以认为,使用最新的JDK,使用G1垃圾回收器。开始这两个字符串还是占用不同的内存。
只有在执行垃圾回收后,这两个字符串才占用相同的内存?
返回顶部
顶部