原文链接: 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. (超级详细版)利用ThinkPHP3.2.3+PHPExcel实现将表格数据导入到数据库

    请先阅读以下步骤再到结尾下载源码 第一步:下载 thinkphp_3.2.3 和 PHPExcel_1.8.0 并解压 对应的网站分别为: http://www.thinkphp.cn/down.ht ...

  2. 【js】vue 2.5.1 源码学习 (九) 响应数组对象的变

    大体思路(八) 本节内容: 1.Observe 如何响应数组的变化 代理原型 数组变异方法 shell cacheArrProto methods 新添加的数组需要加到显示系统里面,拦截 push等的 ...

  3. P1072 城市轰炸

    题目描述 一个大小为N*M的城市遭到了X次轰炸,每次都炸了一个每条边都与边界平行的矩形. 在轰炸后,有Y个关键点,指挥官想知道,它们有没有受到过轰炸,如果有,被炸了几次,最后一次是第几轮. 输入格式 ...

  4. P1018 灵灵排数字

    题目描述 今天灵灵收到了n张卡片,他需要给他们从小到大排序. 输入格式 输入的第一行包含一个整数 \(n(1 \le n \le 10^5)\) . 输入的第二行包含 \(n\) 个正整数,以空格间隔 ...

  5. 浅谈javaScript数据类型、变量、内存之间的关系,文末有图解

    一.变量是没有类型的 在JavaScript中,定义变量的方法是“var 变量名=变量值”,无论这个变量要给他赋值为一个数字.字符串还是数组,他的类型都不需要声明.也就是说如果我只声明了一个变量“va ...

  6. linux进程延迟

    #include <linux/wait.h> long wait_event_interruptible_timeout(wait_queue_head_t *q, condition, ...

  7. 前端小白-----ES6之字符串模板

    前言:只要坚持就会胜利--Coldfront-小白菜 既是总结也是一种分享 分享内容:ES6 字符串模板 案例1:var Musics=[{music:"六月的雨",singer: ...

  8. Koa搭建简单服务器

    1. dependencies "co-mysql": "^1.0.0", "koa": "^2.7.0", " ...

  9. C++动态数组中的C6385, C6386警告

    警告 C6385 从“m”中读取的数据无效: 可读大小为“col*sizeof(int)”个字节,但可能读取了“8”个字节. 警告 C6386 写入到“m”时缓冲区溢出: 可写大小为“col*size ...

  10. 网上做题随笔--MySql

    网上写写题 提高下自己的能力. Mysql平时写的是真的很少,所以训练一下下. 1.查找重复的电子邮箱 https://leetcode-cn.com/problems/duplicate-email ...