java实现引用的方式
java实现引用有直接指针和句柄池2种方式,下面简单说下2种方式的区别和优缺点:
- 直接指针的实现方式是把obj的内存地址或者相对偏移量直接赋值给Java变量,其优点是直接定位,访问快,但是缺点也很明显,Java是一门gc语言,当发生gc时,需要移动obj的偏移量,那么,对应的指针也需要发生移动,而直接指针实现的引用,需要修改所有持有该引用的变量,虚拟机需要去找到那些变量,所以实现起来很难。
- 句柄池的实现是在虚拟机内部维护一个用来放句柄的池子,句柄是可以直接赋值给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步:
- 获取句柄关联的指针
- 调用stringTable的intern方法(真正的intern逻辑)
- 将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直接拷贝一份到永久代。