JVM系列3---字符串和常量池

本篇博客主要从jvm层面去分析Java字符串在Java内存区域的存储方式

常量池


  • class文件常量池: 存放在class文件的静态常量池,相当于内存里面的一个东西序列化到这里面,到内存之后可能会以某种合适的数据结构来存储或索引
  • 运行时常量池:
    • InstanceKlass的一个属性
    • 存在于方法区(元空间)
  • 字符串常量池:
    • 这个是本文的重点,字符串常量池存在于堆中
    • 字符串常量池的本质是存放于堆中的HashTable

Klass与oop(对String来说)


  • Klass:JVM里面(C++代码)的Klass代表了Java中的Class类

  • oop: 这篇博客的主要内容是字符串,暂时不提oop体系,但是为了之后的叙述方便,这里先说明,在JVM中String对应的实例时instanceOopDesc(别的类的对象也是用这个来表示,也就是这个是Java对象在JVM中的存在形式)

字符串常量池


  • HashtableEntry组成

    • key: 每个键值对的key为hashValue,这个hashValue是由一个哈希函数对字符串进行操作得出来的,然后HashTable可以用这个值来找到底层数组的索引,类似HashMap

    • Key的生成方式:1. 通过String的内容和长度生成hash值 2.将hash值转化为Key(取模就行)

      ```c++
      

    hashValue = hash_string(name, len);
    index = hash_to_index(hashValue);

    // Pick hashing algorithm
    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);
    }
    // Bucket handling
    int hash_to_index(unsigned int full_hash) {
    int h = full_hash % _table_size;
    assert(h >= 0 && h < _table_size, “Illegal hash value”);
    return h;
    }

    
    * value: 键值对的value是String,在jvm中也就是instanceOopDesc
    
    * key-value entry的生成方式如下
    
    ```c++
    HashtableEntry* entry = new_entry(hashValue, string());
    add_entry(index, entry);
    
    template  HashtableEntry* Hashtable::new_entry(unsigned int hashValue, T obj) {
      HashtableEntry* entry;
    
      entry = (HashtableEntry*)BasicHashtable::new_entry(hashValue);
      entry->set_literal(obj);
      return entry;
    }
    

Java生成字符串,底层发生了什么?


String s = "11"形式


public static void test1(){
     
        String s1 = "11";
        String s2 = "11";


        System.out.println(s1==s2);
        System.out.println(s1.equals(s2));
    }

上面的代码发生了如下的事情:

  • JVM系列3---字符串和常量池_第1张图片
  • 内存的情况如上图所示当运行到String s1 = “11”;的时候,jvm对于这行代码的等式右边的"11",做的事情首先是利用字符串求出哈希值,然后找到在hashtable的对应位置,判断他是否在常量池中出现过,如果没有出现过,这时候JVM会利用这个值先生成一个char数组,这个char数组存在于堆里面(java里面这些东西都是对象),然后再创建一个String,这个String保持了对刚刚那个char[]的引用,然后这时候做完这个事情之后,就把这个生成的String引用赋值给s1
  • 等到了String s2 = “11”;这一步,也是按照刚刚的流程走,只不过这一次,由于刚刚在常量池生成了,所以这次查询HashTable的结果是存在,所以直接就把那个entry里面的value的地址赋值给s2,这样一来,s2,s1引用了同一块内存,那么他们两个==的结果就是true
  • 执行完了这两部操作,对于JVM来说,增加了一个String对象,2个oop对象(包括String和char[])

String s = new String(“11”);形式


public static void test2(){
     
	String s1 = new String("11");
	String s2 = new String("11");
	System.out.println(s1==s2);
	System.out.println(s1.equals(s2));
}

JVM系列3---字符串和常量池_第2张图片

  • 对于这幅图的理解:首先执行到括号里面的"11"的时候,执行的依然是刚刚那种第一种情况,去HashTable看看是否有存在过,如果不存在的话就创建一个String,这个创建依然是多一个String对象,多两个oop对象
  • 然后执行到new String(“11”),new实际上就是去堆去生成一个对象,利用的是String的构造函数,而他的构造函数里面有一个以单个String为参数的,底层大概做的事情就是把传入的参数的char[]引用赋值给新的String,这一步多了1个String,1个oop(就是那个String)
  • 然后到String s2 = new String(“11”);的时候,也和上一步差不多,这不过执行到这个"11"的时候HashTable命中了,直接就返回了entry里面的那个String指针,但是由于是new对象,所以这个生成新的String对象的操作还是有的,而且由于构造函数的原因,这个新生成的String的char[]引用还是和常量池那个"11"的一样的
  • 那么在这个程序中由于地址不一样,所以==的运算结果为false,但是由于String重写了equals方法,只要字符串内容一样,算出来的哈希值也就一样,那equals是可以比较字符串内容的
  • 这个程序生成了3个String,4个oop对象(3String+1char[])

字符串拼接

public static void test3(){
     
    String s1 = "1";
    String s2 = "1";
    String s = s1+s2;

	//s.intern();

	String str = "11";
	System.out.println(s==str);
}
  • 先说一下结果,结果是false
  • s1+s2底层是怎么做的?通过分析字节码可以知道,jvm是通过生成一个StringBuilder,然后用StringBuilder的append执行两次,然后在用StringBuilder的toString()方法,而这个toString()方法点进去看可以知道他会new一个String,用的构造函数是三个参数的构造函数,3个参数的构造函数和1个函数的构造函数的区别就在于,他没有在常量池中生成记录,执行这个构造方法之后,oop增加了两个,String对象增加了一个
  • 但是如果把s.intern()的注释去掉,这个结果就为true,原因请看如下分析
    • s.intern()的作用就是,如果常量池中有就直接返回地址,如果常量池中没有就生成一个entry然后放进去,这个entry的value 就是s
    • 那么在执行到"11"的时候由于常量池此时是有值的,所以str引用的是常量池的那个String,而刚刚intern()放进去的时候

编译期确定字符串值的情况(final)


public static void test4(){
     
        final String s1 = "1";
        final String s2 = "1";
        String s = s1+s2;

        //s.intern();

        String str = "11";

        System.out.println(s == str);
    }
  • 由于两个final拼接,在编译期的时候就可以确定s的值,这里jvm把他存在常量值里面,那么由于在到"11"的时候常量值有值,所以str拿到的也是那个常量池的那个地址

编译期不确定字符串常量值的情况(final)


public static void test5(){
     
        final String s1 = new String("1");
        final String s2 = new String("1");
        String s = s1+s2;

        //s.intern();

        String str = "11";
java
        System.out.println(s == str);
    }
  • 虽然用到了final,但是new String创建了字符串,在编译期无法确定值,然后jvm的处理就不会把他放在常量池中(放的话是特殊优化处理),所以结果为false,一个String在常量池,前面的那个是由刚刚说的StringBuilder拼出来的不放在常量池中

你可能感兴趣的:(JVM,java)