原文链接: java-performance 翻译: ImportNew.com夏千林
译文链接: http://www.importnew.com/7656.html

ChangeLog:

  • 2013年11月19日,更新了Java8版本中的变化。
  • 013年11月28日,更新了Java 7u40版本中的变化。(感谢Sunny Chan以及他的同事提示我关注新版本的JDK)

共享一个基础char[] 

原先的String类中有4个非静态变量:

  • char[] value用于存储字符串。
  • int offset用于记录字符串首字母在value数组中对应的下标。
  • int count用于记录字符串的长度。
  • int hash用于缓存该字符串的哈希值。

正如你所看到的,绝大多数String对象都会是offset=0并且count=value.length。除非通过调用String.substring方法创建的String对象,或是间接调用Pattern.split这类API创建对象。

String.substring创建的String对象将和原String对象共享同一个内部变量char[] value,这样设计的好处是:

  1. 通过共享字符串节省内存开销。
  2. String.substring方法的时间复杂度为O(1)。

然而,这样的设计有可能会导致内存泄露:如果你从一个长度很长的String对象中提取出一个很短的子串,当这个String对象不再需要时(该对象静候GC回收),你的子串中还保持着这个String对象中存储着完整字符串的char[] value数组的引用。这种情况的解决方法是通过构造函数new String(String)创建一个新的子串对象,进而解除短子串与长母串之间的依赖关系。

自Java 1.7.0_06版本起(包括Java 8的最新版本),String类中不再有offset和count变量。这意味着成员变量char[] value将不会被共享。你可以忘记上述有关内存泄露的描述以及如何使用new String(String)方法来避免内存泄露的发生。但需要记住的是,String.substring现在是线性级的时间复杂度,不再是常数级的时间复杂度。

哈希算法的改变

下面的部分仅适用于Java 7u6以上的Java 7版本,这些代码在Java 8中已被删除。

String类在本次更新中的另一个变化是一个新的哈希算法。Oracle表示新的算法能生成出更好的哈希分布,并且能够提高基于哈希算法的容器的性能,如HashMap、Hashtable、HashSet、 LinkedHashMap、LinkedHashSet、WeakHashMap和ConcurrentHashMap 。与本文第一部分介绍的变化不同,这部分变化是试验性质的,默认是关闭的。

你可能已经猜到了,这部分变化只适用于String类型的Key。如果想要启用它们,需要将系统变量jdk.map.althashing.threshold设置为一个非负的整数值(默认为-1)。当使用新的哈希方法时,这个值将是容器大小的阈值。这里需要注意的是:哈希方法只有在进行重哈希(rehash)的时候才会被更新。因此,如果容器上次执行重哈希是在size=160的时候,而jdk.map.althashing.threshold = 200,这样只有在容器的size增长到大约320的时候,哈希方法才会被更新。

String类现在有一个hash32()方法,它的结果被缓存在成员变量int hash32 中。这个方法最大的变化是,同一个字符串在不同的JVM上执行hash32()的结果可能不同(确切的说,多数情况下都会不同,因为其内部分别调用了一次System.currentTimeMillis()和两次System.nanoTime()用于初始化seed)。因此,某些容器在每次运行程序时迭代顺序都不同。

事实上,我对这个方法的改变有一点意外。如果原先的hashCode方法运行的很好,为什么我们需要一个新的哈希方法呢?我决定使用文章hashcode方法性能调优中的测试程序,测试一下使用hash32方法会产生多少个重复的哈希值。

String.hash32()方法不是公有的,因此我只能通过查看HashMap的源码来找到调用String.hash32()的方法。答案是issun.misc.Hashing.stringHash32(String)。

使用同一数据集(由1百万个各不相同的Key组成)进行测试,String.hash32生成了304个重复的哈希值,而相比之下String.hashCode并没有生成重复的哈希值。我想我们需要静候Oracle进一步的完善或者更多的使用场景说明。

新的哈希算法可能会严重影响高并发、多线程代码

本章节适用于Java 7版本的build 6(包含build 6)至build40(不包含build40)。这部分代码在Java 8中已被删除。有关Java 7u40以上版本的相关介绍请参见下一个章节。

Oracle在后面这些类的哈希实现中遗留了一个bug: HashMap、Hashtable、HashSet、LinkedHashMap、LinkedHashSet和WeakHashMap,只有ConcurrentHashMap 不受影响。这是因为所有的非concurrent类中现在都引入了下面的成员变量:

1
2
3
4
5
/**
 * A randomizing value associated with this instance that is applied to
 * hash code of keys to make hash collisions harder to find.
 */
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

这意味着每一个map或set实例创建的过程中都会调用sun.misc.Hashing.randomHashSeed方法。randomHashSeed后续会调用java.util.Random.nextInt方法。Random类以其多线程环境下不友好而闻名:它有一个Atomic类型的成员变量private final AtomicLong seedfield。Atomic类型在多线程竞争程度较低或者适中的场景下性能表现较好,但在竞争激烈的场景下性能很差。

因此,很多处理HTTP/JSON/XML请求的高负载Web应用可能会被这个bug所影响,因为现有的解析器在表示名值对(name-value)时几乎都使用了上述的存在bug的容器。这些解析器还很可能使用了嵌套的map,这会进一步增加每秒中创建map实例的数量。

如何解决这一问题呢?

1. 使用ConcurrentHashMap :只有在设置系统变量jdk.map.althashing.threshold时才会调用randomHashSeed 方法。但很可惜的是,这种方式仅适用于JDK的核心开发者。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * A randomizing value associated with this instance that is applied to
 * hash code of keys to make hash collisions harder to find.
 */
private transient final int hashSeed = randomHashSeed(this);
 
private static int randomHashSeed(ConcurrentHashMap instance) {
    if (sun.misc.VM.isBooted() && Holder.ALTERNATIVE_HASHING) {
        return sun.misc.Hashing.randomHashSeed(instance);
    }
 
    return 0;
}

2. Hacker的方式:修改sun.misc.Hashing类,这种方式极度不推荐。但如果你依然想解决这个bug,解决的思路是:java.util.Random类并不是final的。你可以在Java 7中加入Random 类的一个Thread Local的子类:java.util.concurrent.ThreadLocalRandom,它内部使用了ThreadLocal<ThreadLocalRandom>(感谢Benjamin Possolo指出我在之前的文章中遗漏了这个类的介绍)。除此之外,ThreadLocalRandom属于CPU cache感知型:每个ThreadLocalRandom实例的seed后面增加了64字节的填充(cache行的大小),进而降低2个不同的seed在同一个cache行中碰撞的可能性。

然后你可以修改成员变量sun.misc.Hashing.Holder.SEED_MAKER,将它初始化为Random子类的实例(ThreadLocalRandom)。不用担心这个变量是私有的、静态的而且是final的,反射机制可以帮你

1
2
3
public class Hashing {
    private static class Holder {
        static final java.util.Random SEED_MAKER;

Java 7u40以上的版本中新的哈希算法不再影响高并发、多线程代码

Oracle在Java 7u40版本中修正了上述的bug

他们使用了上一章节中提到的方法一,仅在重哈希阀值被启用时(通过设置系统变量jdk.map.althashing.threshold启用)才调用sun.misc.Hashing.randomHashSeed方法。Oracle只修改了两个类:HashMap和Hashtable,进而间接修改了 HashSet、LinkedHashMap和LinkedHashSet,因为这三个类是基于HashMap实现的。唯一没有被修改的类是WeakHashMap,但我实在想不出这个类会被大量实例化的应用场景。

相关文章

最近,本文在Reddit上引起了激烈的讨论。我推荐读者们去看一看:

总结

  • 自Java 1.7.0_06版本起,String.substring方法会为每个子串创建一个新的char[] value(而不是共享母串的char[] value)。这意味着String.substring方法的时间复杂度由常数阶变为线性阶。这种变化的好处是String对象占用的内存稍微少了一些(比以前少8个字节),同时确保String.substring方法不会导致内存泄漏(有关Java对象内存布局的详细信息,请见String packing part 1: converting characters to bytes)。
  • Java 7u6+版本中的功能,在Java 8中被删除。自Java 1.7.0_06版本起,String类有了第二个哈希函数:hash32。该方法目前还不是公有的,只能通过使用反射机制或者是调用sun.misc.Hashing.stringHash32(String)来访问该方法。只有当那7种哈希相关的JDK容器的大小超过系统变量jdk.map.althashing.threshold所设定的阀值时,该方法才会被使用。这是一个试验性质的功能,目前我不推荐在代码中使用这一功能。
  • Java 7u6 (包含Java 7u6)至Java 7u40(不包含Java 7u40)版本中的功能,不适用于Java 8。新的哈希实现引入了一个性能上的bug,这个bug涉及Java 7u6 (包含Java 7u6)到Java 7u40(不包含Java 7u40)之间所有版本中所有标准的非concurrent的Map和Set容器。这个bug只影响多线程应用每秒钟创建Map实例的效率。详情请见本文第三章节。Java 7u40版本已修复这个bug。

Java 1.7.0_06中String类内部实现的一些变化【转】的更多相关文章

  1. java中String类学习

    java中String类的相关操作如下: (1)初始化:例如,String s = “abc”; (2)length:返回字符串的长度. (3)charAT:字符操作,按照索引值获得字符串中的指定字符 ...

  2. 在java中String类为什么要设计成final?

    大神链接:在java中String类为什么要设计成final? - 程序员 - 知乎 我进行了重新排版,并且更换了其中的一个例子,让我们更好理解. String很多实用的特性,比如说“不可变性”,是工 ...

  3. Java中String类的方法及说明

    String : 字符串类型 一.      String sc_sub = new String(c,3,2);    //      String sb_copy = new String(sb) ...

  4. 在java中String类为什么要设计成final

    在java中String类为什么要设计成final? - 胖胖的回答 - 知乎 https://www.zhihu.com/question/31345592/answer/114126087

  5. 【转载】Java中String类的方法及说明

    转载自:http://www.cnblogs.com/YSO1983/archive/2009/12/07/1618564.html String : 字符串类型 一.      String sc_ ...

  6. Java中String类为什么被设计为final?

    Java中String类为什么被设计为final   首先,String是引用类型,也就是每个字符串都是一个String实例.通过源码可以看到String底层维护了一个byte数组:private f ...

  7. 来说说Java中String 类的那些事情

    今天正好学校那边的任务不多,我就打算把Stirng 的有关知识点都总结在一起了,这样有利于知识的系统性,要不然学多了就会越来越杂,最主要的是总会忘记,记忆的时间太短了,通过这种方式,把它归纳在一起,写 ...

  8. 关于Java中String类的hashCode方法

    首先来看一下String中hashCode方法的实现源码 public int hashCode() { int h = hash; if (h == 0 && value.lengt ...

  9. JDK6与JDK7中String类subString()方法的区别

    1.subString()方法的作用 subString(int beginIndex, int endIndex)方法的返回的是以beginIndex开始到 endIndex-1结束的某个调用字符串 ...

随机推荐

  1. fatal: Not a git repository (or any of the parent directories)

    当从github.com上面下载下了Firmware后,无意中删除了Firmware目录下的.git文件夹,再去编译就会出现:   fatal: Not a git repository (or an ...

  2. eclipse本地启动tomcat报错集锦

    1.eclipse本地添加tomcat服务器    打开Eclipse,单击“window”菜单,选择下方的“Preferences”: 找到Server下方的Runtime Environment, ...

  3. js基础——正则表达式

    1.创建方式: var box = new RegExp('box');//第一个参数字符串 var box = new RegExp('box','ig');//第二个参数可选模式修饰符 等同于 v ...

  4. vuex 快速上手,具体使用方法总结(含使用例子)

    网上有关vuex的文章很多,但有些比较复杂,这篇文章能让你快速使用vuex: vuex 用处:管理全局状态(类似全局变量,每个组件都能访问到) vuex 用法: //下面是一个js文件,用最简单最全的 ...

  5. linux进程简单睡眠

    当一个进程睡眠, 它这样做以期望某些条件在以后会成真. 如我们之前注意到的, 任何睡 眠的进程必须在它再次醒来时检查来确保它在等待的条件真正为真. Linux 内核中睡眠的 最简单方式是一个宏定义, ...

  6. slot的使用方法

    参考链接:https://www.cnblogs.com/loveyt/p/9946450.html 插槽的使用其实是很简单,你只需明白以下两点,就很容易理解. 1.插槽是使用在子组件中的, 2.插槽 ...

  7. H3C设置下次启动的配置文件

  8. ASP.NET MVC 实现页落网资源分享网站+充值管理+后台管理(1)之数据库设计

    本文主要讲解本项目网站所应用到的知识点,及数据库的相关设计: 一.知识点 (1)本项目主要采取ASP.NET MVC的编程模式,相信你已经了解到了MVC的具体含义是什么,这里不再赘述,有不了解的朋友, ...

  9. jQuery 工具类函数-URL操作函数

    调用名为$. param的工具函数,能使对象或数组按照key/value格式进行序列化编码,该编码后的值常用于向服务端发送URL请求,调用格式为: $. param (obj); 参数obj表示需要进 ...

  10. KETTLE4个工作中有用的复杂实例--1、数据定时自动(自动抽取)同步作业

    今天呕心沥血花了8个小时给大家带来kettle工作中最常见的四种复杂实例,90%的项目用到这4种实例都可以解决. 4种实例种还有2种通用kettle工具,使用这两种通用工具实例,可以直接修改相应的配置 ...