openjdk hotspot源码分析之string.intern方法原理

java实现引用的方式

java实现引用有直接指针和句柄池2种方式,下面简单说下2种方式的区别和优缺点:

  1. 直接指针的实现方式是把obj的内存地址或者相对偏移量直接赋值给Java变量,其优点是直接定位,访问快,但是缺点也很明显,Java是一门gc语言,当发生gc时,需要移动obj的偏移量,那么,对应的指针也需要发生移动,而直接指针实现的引用,需要修改所有持有该引用的变量,虚拟机需要去找到那些变量,所以实现起来很难。
  2. 句柄池的实现是在虚拟机内部维护一个用来放句柄的池子,句柄是可以直接赋值给Java变量的,那么怎么和指针关联呢?句柄其实会关联一个指针,因此Java的变量指向句柄,而句柄是和指针关联的。其优点很明显,由于指针都由常量池维护,移动obj偏移量时,只需要去句柄池找到对应的句柄,修改指向的地址,这种实现是很高效的。

在这里插入图片描述

java.lang.String.intern

这篇文章的重点是讲string intern方法的源码,但是这篇文章的开头我在说Java引用的实现方式,这是因为理解Java实现引用的方式是看本地方法所需要的基础。我们直接看intern的native方法

//str其实就是调用intern方法的string,只不过这里的str实际上是个句柄
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); //获取str句柄的指针
  oop result = StringTable::intern(string, CHECK_NULL);//调用stringTable的intern方法获取string的指针
  return (jstring) JNIHandles::make_local(env, result); //将string指针封装成句柄并返回
JVM_END

java.lang.String的intern的本地方法很简单,主要分为3步:

  1. 获取句柄关联的指针
  2. 调用stringTable的intern方法(真正的intern逻辑)
  3. 将intern返回的指针和句柄关联,并返回句柄

我们接着往下看stringTable的intern方法:

oop StringTable::intern(oop string, TRAPS)
{
  if (string == NULL) return NULL;
  ResourceMark rm(THREAD);
  int length;
  Handle h_string (THREAD, string); //将string指针包装成句柄
  jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL); //获取string的char数组
  oop result = intern(h_string, chars, length, CHECK_NULL); //调用重载的intern方法
  return result;
}

这里的代码逻辑还是十分简单,首先将string指针包装成句柄(这里实在不理解为什么要包装成句柄),然后获取char数组和string的char数组的长度。然后调用重载的intern方法,所传的参数是句柄、char数组、char数组的长度,重载的intern方法返回一个指针,这里直接将指针返回给string.intern的native方法。下面我们再来看看重载的intern方法:

oop StringTable::intern(Handle string_or_null, jchar* name,
                        int len, TRAPS) {
  // shared table always uses java_lang_String::hash_code
  unsigned int hashValue = java_lang_String::hash_code(name, len); //获取string的hash值

  oop found_string = lookup_shared(name, len, hashValue); //查找stringTable有没有指向的内存地址的内容和char数组的值相等的指针
  if (found_string != NULL) { //如果引用不为空,直接返回
    return found_string;
  }
  if (use_alternate_hashcode()) {
    hashValue = alt_hash_string(name, len);
  }
  int index = the_table()->hash_to_index(hashValue); //根据hash值计算索引值,从这里其实就可以看出来stringTable实际上就是一个hashtable
  found_string = the_table()->lookup_in_main_table(index, name, len, hashValue); //根据索引和char数组以及数组长度去stringTable里面查找有没有指向的内存地址的内容和char数组内容相同的指针
  //这里为什么会有2个stringTable

  // Found
  if (found_string != NULL) {
    if (found_string != string_or_null()) {
      ensure_string_alive(found_string);
    }
    return found_string; //如果找到了指向的内容和char数组内容相同的指针,直接返回
  }

  debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
  assert(!Universe::heap()->is_in_reserved(name),
         "proposed name of symbol must be stable");

  Handle string;
  // try to reuse the string if possible
  if (!string_or_null.is_null()) {
    string = string_or_null;
  } else {
 	// 如果没有在stringTable不存在指向的内容和char数组内容相同的指针,那么就构建一个string的obj,并用句柄关联
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  }

#if INCLUDE_ALL_GCS
  if (G1StringDedup::is_enabled()) {
    // Deduplicate the string before it is interned. Note that we should never
    // deduplicate a string after it has been interned. Doing so will counteract
    // compiler optimizations done on e.g. interned string literals.
    G1StringDedup::deduplicate(string());
  }
#endif
  // Grab the StringTable_lock before getting the_table() because it could
  // change at safepoint.
  oop added_or_found;
  {
    MutexLocker ml(StringTable_lock, THREAD);
    // Otherwise, add to symbol to table
    // 将构建的string的obj加到stringTable
    added_or_found = the_table()->basic_add(index, string, name, len,
                                  hashValue, CHECK_NULL);
  }

  if (added_or_found != string()) {
    ensure_string_alive(added_or_found);
  }
  //返回构建的obj的指针(从句柄里获取)
  return added_or_found;

string的intern的主要逻辑就在这个方法里,先去2个stringTable查找,没有找到,就构建一个string,然后将构建的string的内存地址加入到stringTable,注意,stringTable存的是string的指针,不是string本身。然后将构建的string的指针返回。下面我们来看把string指针加入到stringTable的代码:

oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
                           int len, unsigned int hashValue_arg, TRAPS) {

  assert(java_lang_String::equals(string(), name, len),
         "string must be properly initialized");
  // Cannot hit a safepoint in this function because the "this" pointer can move.
  NoSafepointVerifier nsv;

  // Check if the symbol table has been rehashed, if so, need to recalculate
  // the hash value and index before second lookup.
  unsigned int hashValue;
  int index;
  if (use_alternate_hashcode()) {
    hashValue = alt_hash_string(name, len);
    index = hash_to_index(hashValue);
  } else {
    hashValue = hashValue_arg;
    index = index_arg;
  }

  // Since look-up was done lock-free, we need to check if another
  // thread beat us in the race to insert the symbol.

  // No need to lookup the shared table from here since the caller (intern()) already did
  oop test = lookup_in_main_table(index, name, len, hashValue); // calls lookup(u1*, int)
  if (test != NULL) {
    // Entry already added
    return test;
  }

  // c++语言很强大,可以重写运算符,这里就是一个重写了运算符的例子
  // string是个句柄,句柄重写了(),string()方法返回的是关联的指针,所以这里的hashentry的值是string的指针
  HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
  add_entry(index, entry); //将entry加到stringTalbe
  return string(); //返回句柄关联的指针
}

还有一个很重要的地方,当没有在stringTable里面找到对应的指针时,就构建一个string,那么这个string是在哪里构建的呢。我们继续看源码,如下:

Handle java_lang_String::create_from_unicode(jchar* unicode, int length, TRAPS) {
  bool is_latin1 = CompactStrings && UNICODE::is_latin1(unicode, length);
  Handle h_obj = basic_create(length, is_latin1, CHECK_NH);
  typeArrayOop buffer = value(h_obj());
  assert(TypeArrayKlass::cast(buffer->klass())->element_type() == T_BYTE, "only byte[]");
  if (is_latin1) {
    for (int index = 0; index < length; index++) {
      buffer->byte_at_put(index, (jbyte)unicode[index]);
    }
  } else {
    for (int index = 0; index < length; index++) {
      buffer->char_at_put(index, unicode[index]);
    }
  }

#ifdef ASSERT
  {
    ResourceMark rm;
    char* expected = UNICODE::as_utf8(unicode, length);
    char* actual = as_utf8_string(h_obj());
    if (strcmp(expected, actual) != 0) {
      tty->print_cr("Unicode conversion failure: %s --> %s", expected, actual);
      ShouldNotReachHere();
    }
  }
#endif

  return h_obj;
}

Handle java_lang_String::basic_create(int length, bool is_latin1, TRAPS) {
  assert(initialized, "Must be initialized");
  assert(CompactStrings || !is_latin1, "Must be UTF16 without CompactStrings");

  // Create the String object first, so there's a chance that the String
  // and the char array it points to end up in the same cache line.
  oop obj;
  obj = SystemDictionary::String_klass()->allocate_instance(CHECK_NH);

  // Create the char array.  The String object must be handlized here
  // because GC can happen as a result of the allocation attempt.
  Handle h_obj(THREAD, obj);
  int arr_length = is_latin1 ? length : length << 1; // 2 bytes per UTF16.
  typeArrayOop buffer = oopFactory::new_byteArray(arr_length, CHECK_NH);;

  // Point the String at the char array
  obj = h_obj();
  set_value(obj, buffer);
  // No need to zero the offset, allocation zero'ed the entire String object
  set_coder(obj, is_latin1 ? CODER_LATIN1 : CODER_UTF16);
  return h_obj;
  
instanceOop InstanceKlass::allocate_instance(TRAPS) {
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, this);

  instanceOop i;

  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

我们可以看到内存是在Java的堆里分配的,所以实际上intern方法在stringTable里没有找到对应的指针时,就会去堆里创建对象。

intern方法的调用时机

除了我们在Java里面显示的去调用string的intern方法,其实虚拟机自己也会去调用intern方法,当虚拟机在加载类时,给static final string = “abc” 这种常量初始化值时,就会去调用intern方法,虚拟机在运行字节码的时候也会调用intern方法,比如下面的代码:

public static void main(String[] args){
   String str = "javastring"; 
   //当虚拟机运行到这条指令时,会调用intern方法,获取字符串"javastring"的指针
   //并将指针和句柄关联,然后把句柄赋值给str变量
}

总结

string的intern方法在1.6以后不再拷贝string,而只是将指向string的指针存到stringTable,而且string调用intern时,当没有在stringTable找到时,创建的string是在堆里分配内存的。1.6和1.6以前的intern会把堆里的string直接拷贝一份到永久代。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值