JVM的字符串常量池和hashcode相关学习过程总结

Ⅰ. 背景

  • 在研究String不可变特性的时候, 因为比较好奇有关常量池的相关概念,就深入了一下到JVM源码进行了探究

  • 在研究常量池的过程中,不可避免的又涉及到了Java中hashCode的相关概念,所以就顺带看了下JVM中关于hashCode的本地方法实现

  • 本篇主要是记录一下整个探查的过程,算是给自己一个记录,方便后续查阅。

Ⅱ. 介绍

2.1 字符串常量池

Step 1

  • 提到字符串常量池,我比较好奇它底层的存储数据格式到底是怎么样的?因此就需要深入去探查一下。因为是字符串常量池,所以就自然想到了String的一个方法 intern(),它的作用就是将字符串对象放入到字符常量池中,该方法是一个native方法,因此去找了下openJDK源码,这里用到的是github上openjdk的jdk8-b120 的tag版本

  • String类的全部包名:java.lang.String,所以可以去找open JDK源码中share/native目录下按照包名往下找。

  • 下载并打开该项目后,可以在jdk目录下:

image.png

native目录下一般存放的都是JDK中一些本地方法的对应C语言调用,这里要看的是String类下的intern方法,因此找到java/lang/String.c文件:

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

这里仅仅只是一个调用,具体的实现还得到具体的JVM实现厂商的源代码中去寻找,最常见的肯定就是hotspot虚拟机,这里可以到hotspot目录下去搜索相关实现。

这里全局搜一下JVM_InternString这个内容,可以发现在hotspot的src/share/vm/prims/jvm.cpp中出现了:

// String support ///////////////////////////////////////////////////////////////////////////

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

可以发现有一个StringTable调用了intern方法,那么就可以在项目中全局搜一下 class StringTable这个关键词,就可以看到有一个symbolTable.hpp文件里面出现了这个定义:

class StringTable : public Hashtable<oop, mtSymbol> {
    ......
    static oop intern(oop string, TRAPS);//这个方法的签名和上面的调用是一致的
    ......
}

这里可以看到,StringTable其实就是一个hashTable,内部有一个intern的静态方法,所以可以到对应的cpp文件里面找到具体的实现,下面贴出了部分源码,从上到下是按照一个方法的调用链贴出来的:

oop StringTable::intern(oop string, TRAPS)
{
  if (string == NULL) return NULL;
  ResourceMark rm(THREAD);
  int length;
  Handle h_string (THREAD, string);
  jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL);
  oop result = intern(h_string, chars, length, CHECK_NULL); // 这里调用了当前文件内另外一个intern方法
  return result;
}

oop StringTable::intern(Handle string_or_null, jchar* name,
                        int len, TRAPS) {
    ......
    // 这里的the_table返回的就是当前的StringTable,所以它是调用了StringTable内部的那个basic_add方法
    return the_table()->basic_add(index, string, name, len,
                              hashValue, CHECK_NULL);
}

oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
                           int len, unsigned int hashValue_arg, TRAPS) {
    ......
    HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
    add_entry(index, entry);
    return string();                           
}

这里可以看到,实际上StringTable中的存储是一个个HashtableEntry对象组成的,其中entry的key说字符串的hash值,value则是字符串引用(一种Handle类型)。

所以到了这里基本上就解开了我的疑惑:到底字符串常量池到底说一个什么数据结构存储的?

它实际上就是一系列的k-v组合成的Entry,然后将其通过index将整个entry塞入到hashTable中去;而entry的k-v结构中,k实际上就是字符串的hash值,value就是字符串本身的引用。


image.png

这个图中,String对象内部有一个value属性,它是字符数组,它内部存储的就是真正的字符串内容。当然JDK9开始这个字符数组已经改造为byte数组了,这个和本次主题无关,暂时只考虑JDK8的情况。

Step 2

  • 前面因为看到了字符串常量池的存储格式,所以自然而然就关注到了hashCode,所以对于hashCode又做了一番深入,前面的探索中,在basic_add方法中,可以看到在构造entry对象的时候,key传入的是hashValue,这里的hashValue实际上来自于hash_string这个方法:
if (use_alternate_hashcode()) {
  hashValue = hash_string(name, len);
  index = hash_to_index(hashValue);
} else {
  hashValue = hashValue_arg;
  index = index_arg;
}
......
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
add_entry(index, entry);
return string();
  • 这个hash_string在symbolTable.cpp本身这个文件内就有定义:
unsigned int StringTable::hash_string(const jchar* s, int len) {
  return use_alternate_hashcode() ? AltHashing::murmur3_32(seed(), s, len) :
                                    java_lang_String::hash_code(s, len);
}

这里murmur3_32这块暂时不做考虑,这是hotspot虚拟机这边提供的一种hashCode算法,主要关注的是后面这个:java_lang_String::hash_code(s, len);

这么调用说明hash_code是java_lang_String内的一个静态方法,那就全局搜“class java_lang_String”,可以在javaClasses.hpp文件中找到它的定义:

// Compute the hash value for a java.lang.String object which would
// contain the characters passed in.
//
// As the hash value used by the String object itself, in
// String.hashCode().  This value is normally calculated in Java code
// in the String.hashCode method(), but is precomputed for String
// objects in the shared archive file.
// hash P(31) from Kernighan & Ritchie
//
// For this reason, THIS ALGORITHM MUST MATCH String.hashCode().
template <typename T> static unsigned int hash_code(T* s, int len) {
  unsigned int h = 0;
  while (len-- > 0) {
    h = 31*h + (unsigned int) *s;
    s++;
  }
  return h;
}

这里附带了它的注释,注释里面说明的很清楚:

计算一个包含传入字符的 java.lang.String 对象的哈希值。

String对象本身使用的哈希值是在Java代码中的String.hashCode()方法中计算的,但对于String对象,这个值已经在共享存档文件中预先计算了,使用了 Kernighan & Ritchie 中的hash P(31) 算法。

因此,这个算法必须与String.hashCode()方法匹配。

说白了这里的调用方法实际上就是和JDK中String类中重写的hashCode方法算法是一样的:

s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

这里的“s”就是字符串中的每个字符,n是字符串的长度。

2.2 Object.hashCode()

因为String的hashCode,让我想到了Object中的hashCode,也就是如果没有重写hashCode,默认会调用Object中的hashCode,它不像字符串那样有具体的类型,因此它的hashCode可能就需要基于对象本身做计算,长久以来,我一直以为hashCode的生成和对象的地址有关,但是我这次经过探究以后发现并不是那么简单:

首先看到JDK中Object类中有hashCode的native方法定义:

public native int hashCode();

所以按照前面同样的套路,到openJDK源码中找到java/lang/Object.c文件:

......
static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};
......

需要到hotspot虚拟机中找到“JVM_IHashCode”对应的实现:

//jvm.cpp

// java.lang.Object ///////////////////////////////////////////////

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_IHashCode");
  // as implemented in the classic virtual machine; return 0 if object is NULL
  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END

//同样全局搜关键词“class ObjectSynchronizer”,找到synchronizer.hpp
static intptr_t FastHashCode (Thread * Self, oop obj) ;
//synchronizer.cpp下找到对应实现
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
    ......
    // 这里不考虑各种加锁的情况,只考虑最普通一种情况
    if (mark->is_neutral()) {
      hash = mark->hash();              // this is a normal header
      if (hash) {                       // if it has hash, just return it
        return hash;
      }
      hash = get_next_hash(Self, obj);  // allocate a new hash code
      temp = mark->copy_set_hash(hash); // merge the hash code into header
      // use (machine word version) atomic operation to install the hash
      test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
      if (test == mark) {
        return hash;
      }
      // If atomic operation failed, we must inflate the header
      // into heavy weight monitor. We could add more code here
      // for fast path, but it does not worth the complexity.
    }
    ......
}

核心点可以看到有一个:get_next_hash方法的调用,这里传入的参数是线程对象本身以及待计算hash的对象:

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
    intptr_t value = 0 ;
    if (hashCode == 0) {
       // This form uses an unguarded global Park-Miller RNG,
       // so it's possible for two threads to race and generate the same RNG.
       // On MP system we'll have lots of RW access to a global, so the
       // mechanism induces lots of coherency traffic.
       value = os::random() ;
    } else
    if (hashCode == 1) {
       // This variation has the property of being stable (idempotent)
       // between STW operations.  This can be useful in some of the 1-0
       // synchronization schemes.
       intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
       value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
    } else
    if (hashCode == 2) {
       value = 1 ;            // for sensitivity testing
    } else
    if (hashCode == 3) {
       value = ++GVars.hcSequence ;
    } else
    if (hashCode == 4) {
       value = cast_from_oop<intptr_t>(obj) ;
    } else {
       // Marsaglia's xor-shift scheme with thread-specific state
       // This is probably the best overall implementation -- we'll
       // likely make this the default in future releases.
       unsigned t = Self->_hashStateX ;
       t ^= (t << 11) ;
       Self->_hashStateX = Self->_hashStateY ;
       Self->_hashStateY = Self->_hashStateZ ;
       Self->_hashStateZ = Self->_hashStateW ;
       unsigned v = Self->_hashStateW ;
       v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
       Self->_hashStateW = v ;
       value = v ;
    }

    value &= markOopDesc::hash_mask;
    if (value == 0) value = 0xBAD ;
    assert (value != markOopDesc::no_hash, "invariant") ;
    TEVENT (hashCode: GENERATE) ;
    return value;
}

这里就需要好好说一下了,先忽略掉所有细节,可以看到主要有六个分支,根据hashCode的数值,从0到4外加一个else,这是给出了六种生成策略。

这六种策略分别对应不同的场景:

  1. hashCode=0: os.random(),使用随机数

  2. hashCode=1: 使用对象的地址进行计算

  3. hashCode=2: 始终返回1,主要用于灵敏度测试

  4. hashCode=3: 使用一种序列号进行生成

  5. hashCode=4: 输出对象的内存地址

  6. 其他情况:利用xor-shift算法产生伪随机数

这里我写了一个demo程序进行测试:

Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1.hashCode());
System.out.println(obj2.hashCode());

默认情况下,不做任何配置可以得到:

759156157
1635546341

这里的hashCode值可以通过jvm运行参数进行调整,这里从hashCode=0开始进行测试:

运行前增加参数:

-XX:hashCode=0

发现报错了:

Error: VM option 'hashCode' is experimental and must be enabled via -XX:+UnlockExperimentalVMOptions.
Error: The unlock option must precede 'hashCode'.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit

大概的意思就是说:hashCode还是属于测试性的,如果要使用的话,必须开启UnlockExperimentalVMOptions属性。

所以添加该属性:

-XX:+UnlockExperimentalVMOptions -XX:hashCode=0

得到结果:

// 第一次:
1298937153
829925568
// 第二次:
2065458716
1546335363
// 第三次:
1458888444
146433569

可以看到,每次运行的结果都不一样,符合随机数的特征。

换一种hashCode=1,根据对象的地址进行一定的计算得到的,因为每次运行对象在内存中的地址可能都不一样,所以每次运行结果可能都有不同:

-XX:+UnlockExperimentalVMOptions -XX:hashCode=1
// 第一次
668599630
668599628
//第二次
629723300
629723302
// 第三次
1645286083
1645286081

hashCode=2,灵敏度测试,每次应该都返回1:

-XX:+UnlockExperimentalVMOptions -XX:hashCode=2

// 第一次
1
1
//第二次
1
1
// 第三次
1
1

从结果上看,没有问题。

hashCode=3,使用的是一种序列号:

-XX:+UnlockExperimentalVMOptions -XX:hashCode=3

// 第一次
902
903
//第二次
899
900
// 第三次
899
902

hashCode=4: 输出对象的内存地址

-XX:+UnlockExperimentalVMOptions -XX:hashCode=4

// 第一次
260070984
260071000
//第二次
260070656
260070672
// 第三次
260070984
260071000

hashCode>4: 伪随机数,生成一种和线程状态相关的整数

-XX:+UnlockExperimentalVMOptions -XX:hashCode=5

// 第一次
759156157
1635546341
//第二次
759156157
1635546341
// 第三次
759156157
1635546341

这个结果对比前面默认情况下不加任何参数得到的结果来看,两者的结果是一样的,因此可以推论出:当前我的JDK8版本默认的hashCode值是大于4的,通过网上搜索的一些信息来说,JDK8默认设置hashCode=5,这个结果看来是不矛盾的。

关于这个hashCode的值,可以到hotspot目录下找到:“src/share/vm/runtime/globals.hpp”文件下,找到下面这段代码:

product(intx, hashCode, 5,                                                \
        "(Unstable) select hashCode generation algorithm")

可以看到这里传入的是5,也就是说默认情况下,默认传入的hashCode是5,但是我同时了解到,JDK6和7的版本与JDK8默认的hashCode是不一样的,这里可以直接到openJDK官网上去找对应的源码就可以找到,目的地址一样hotspot/src/share/vm/runtime/globals.hpp:

JDK7:https://github.com/openjdk/jdk/tree/jdk7-b147

JDK6:https://github.com/openjdk/jdk6

//JDK7:
product(intx, hashCode, 0,                                                \
         "(Unstable) select hashCode generation algorithm" )

//JDK6
product(intx, hashCode, 0,                                                \
         "(Unstable) select hashCode generation algorithm" ) 

这里可以看到,对于JDK6和7,默认的hashCode生成走的是前面六种策略的第一种:os.random()。可以发现实际上默认的hashCode生成和对象地址压根儿没什么关系。

2.3 hashCode=5

对于JDK8以及以后版本默认使用的最后一种hashCode生成策略,源码:

// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;

这里的Self类型是:Thread *。自然联想到查看一下是否有thread.cpp这类的文件,搜索后还真发现了在hotspot下面有这么一个文件,打开后没什么发现,顺带就打开了它的头文件thread.hpp,搜索_hashStateX后,有了如下的发现,这里选择了hotspot/src/share/vm/runtime/thread.hpp:

// thread-specific hashCode stream generator state - Marsaglia shift-xor form
_hashStateX = os::random() ;
_hashStateY = 842502087 ;
_hashStateZ = 0x8767 ;    // (int)(3579807591LL & 0xffff) ;
_hashStateW = 273326509 ;

所以可以看到,这里的 Y、Z、W都是固定值,只有X上使用的随机值。因此上面的计算中,核心的那句计算方法实际上应该是:

random = os::random();
v = (273326509 ^ (273326509 >> 19)) ^ (random ^ (random >> 8));

这里的os::random调用,实际上是来自于:hotspot/src/share/vm/runtime/os.cpp内实现的random方法,下面的代码中对注释做了处理,中文部分的注释是手动添加的:

static long random();   // 它返回的是一个32bit的伪随机数

// 对应cpp文件中的实现源码:
long os::random() {
  /* 标准、众所周知的线性全等随机生成器
   * next_rand = (16807*seed) mod (2**31-1)
   * see(这里注释中放了两个参考资料,但是没有地址,经过搜索已经查到对应的论文地址,贴在下面)
   * (1) https://dl.acm.org/doi/10.1145/63039.63042,
   * (2) https://dl.acm.org/doi/10.1145/76372.76379, pp. 87-88.
  */
  const long a = 16807;
  const unsigned long m = 2147483647;
  const long q = m / a;        assert(q == 127773, "weird math");
  const long r = m % a;        assert(r == 2836, "weird math");

  // compute az=2^31p+q
  unsigned long lo = a * (long)(_rand_seed & 0xFFFF); //这里的_rand_seed是前面定义的1.
  unsigned long hi = a * (long)((unsigned long)_rand_seed >> 16);
  lo += (hi & 0x7FFF) << 16;

  // if q overflowed, ignore the overflow and increment q
  if (lo > m) {
    lo &= m;
    ++lo;
  }
  lo += hi >> 15;

  // if (p+q) overflowed, ignore the overflow and increment (p+q)
  if (lo > m) {
    lo &= m;
    ++lo;
  }
  return (_rand_seed = lo);
}

Ⅲ. 总结

  • 首先就是字符串常量池的内部,实际上是一个hashTable结构,这个hashTable内部是以Entry的形式存储的,Entry中有key和value,其中的key是字符串的hash值,value是字符串对象的引用地址

  • String重写了hashCode方法,是通过一个计算公式计算出每个String对象的hashCode值的:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
  • 对于Object中定义的hashCode方法,它是native方法,需要到对应的JDK源码中查看对应的实现

  • 目前hotspot虚拟机中,hashCode的实现方案有六种:

    • hashCode=0: os.random(),使用随机数

    • hashCode=1: 使用对象的地址进行计算

    • hashCode=2: 始终返回1,主要用于灵敏度测试

    • hashCode=3: 使用一种序列号进行生成

    • hashCode=4: 输出对象的内存地址

    • 其他情况:利用xor-shift算法产生伪随机数

  • hashCode的值可以通过jvm的启动参数-XX:hashCode=?来进行设置,对于JDK8以及以后的版本,默认的hashCode值都是5,JDK6和7默认是0。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容