当前位置: 移动技术网 > IT编程>开发语言>Java > 从JVM看String及intern方法

从JVM看String及intern方法

2020年07月16日  | 移动技术网IT编程  | 我要评论

前言

学习JVM过程中,遇到String的intern()方法,然后在网上找了很多发现都不怎么系统,很多说法也都不一致。所以笔者决定研究一下,以此记录下来。

先看一下intern()的意义

简单说就是放入字符串常量池

实现方式的演变

在这里插入图片描述

  • JDK1.7之前:调用这个方法,先去字符串常量池中看是否已经存在,如果已经存在,那么直接返回这个常量在字符串常量池中的地址值,如果不存在,则在字符串常量池中创建一个,并返回其地址值。
  • JDK1.7及之后:调用这个方法,先去字符串常量池中看是否已经存在,如果已经存在,那么直接返回这个常量在字符串常量池中的地址值,如果不存在,则在字符串常量池中保存一份堆中的引用,并返回其地址值。
    这也是JDK7对常量池保存内容的优化:通过字面量(常量)的方式和new String()的方式创建字符串,常量池保存的是对象;通过intern()方法,常量池保存的引用(一份堆中的引用的复制)。
    以下均在JDK7以后版本分析~

创建String的几种方式

  • String str= "abc":通过字面量的方式。先看常量池中是否已经存在相同字符串 (equals()),若存在,返回常量池中的引用。若不存在,在常量池中创建字符串对象并返回其引用。
  • String str = new String("abc"):通过显式创建字符串对象的方式。先在堆中创建一个对象,再看常量池中是否已经存在相同字符串,若存在,直接返回堆中的引用。若不存在,在常量池中创建字符串对象并返回堆中的引用。
  • String str = "abc" + "de":通过常量拼接的方式。会编译优化,等效于String str = "abcde"
  • String str = new String("abc") + new String("de"):涉及对象的拼接。底层是通过创建StringBuilder对象用append方法,最后通过toString转成String(只有new String()才会在常量池中创建对象)。共创建5个对象:堆中(“abc”,“de”,“adbde”),字符串常量池中(“abc”,“de”)。
    这也说明了,在循环体中无论用哪种形式拼接字符串,都会创建很多对象,浪费内存,影响性能~

通过4个例子完整分析intern()影响的内存结构

System.identityHashCode(obj)表示对象引用地址的 hashcode。
常量池逻辑属于方法区,物理上存在于堆中,为方便描述下面不予区分了。

  • eg1
    @Test
    public void function1() {
        String str1 = new String("abc");
        String str2 = "abc";
        String intern = str1.intern();
        System.out.println(str1==str2);//false
        System.out.println("str1:   "+System.identityHashCode(str1));//1018081122
        System.out.println("str2:   "+System.identityHashCode(str2));//242131142
        System.out.println("intern: "+System.identityHashCode(intern));//242131142
    }

在这里插入图片描述
String str1 = new String("abc")在堆和常量池各创建一个对象,str1指向堆。
String str2 = "abc"在常量池中找到了"abc"的引用,所以直接返回常量池的引用。
String intern = str1.intern()在常量池中找到了"abc"的引用,所以直接返回常量池的引用。

  • eg2
    @Test
    public void function2() {
        String str1 = new String("abc");
        String intern = str1.intern();
        String str2 = "abc";
        System.out.println(str1==str2);//false
        System.out.println("str1:   "+System.identityHashCode(str1));//1018081122
        System.out.println("str2:   "+System.identityHashCode(str2));//242131142
        System.out.println("intern: "+System.identityHashCode(intern));//242131142
    }

这里只是改变了第2,3行顺序,但结构同eg2完全一样。

  • eg3
    @Test
    public void function3() {
//        String hel = new String("hel");
//        String lo = new String("lo");
//        String str1 = hel+lo;
        String str1 = new String("hel") + new String("lo");
        String intern = str1.intern();
        String str2 = "hello";
        System.out.println(str1 == str2);//true
        System.out.println("str1:   " + System.identityHashCode(str1));//1018081122
        System.out.println("str2:   " + System.identityHashCode(str2));//1018081122
        System.out.println("intern: " + System.identityHashCode(intern));//1018081122
//        System.out.println(System.identityHashCode(hel));//242131142
//        System.out.println(System.identityHashCode(hel.intern()));//1782113663
//        System.out.println(System.identityHashCode(lo));//1433867275
//        System.out.println(System.identityHashCode(lo.intern()));//476800120
    }

释放注释,则可打印详细的对象的内存地址hashcode。
在这里插入图片描述
String str1 = new String("hel") + new String("lo");共创建5个对象:堆中(“hel”,“lo”,“hello”),字符串常量池中(“hel”,“lo”)。str1指向堆。
String intern = str1.intern();在常量池中没找到"hello"的引用,所以在常量池中保存一份hello对象的引用,并返回该引用地址。
String str2 = "hello";在常量池中找到了"abc"的引用,所以直接返回常量池的引用。
str1指向堆的引用,intern 和str2指向的是str1的堆中引用的复制品,所以都是相等的。

  • eg4
    改变了第2,3行顺序
    @Test
    public void function4() {
//        String hel = new String("hel");
//        String lo = new String("lo");
//        String str1 = hel+lo;
        String str1 = new String("hel") + new String("lo");
        String str2 = "hello";
        String intern = str1.intern();
        System.out.println(str1 == str2);//true
        System.out.println("str1:   " + System.identityHashCode(str1));//1018081122
        System.out.println("str2:   " + System.identityHashCode(str2));//242131142
        System.out.println("intern: " + System.identityHashCode(intern));//242131142
//        System.out.println(System.identityHashCode(hel));//242131142
//        System.out.println(System.identityHashCode(hel.intern()));//1782113663
//        System.out.println(System.identityHashCode(lo));//1433867275
//        System.out.println(System.identityHashCode(lo.intern()));//476800120
    }

在这里插入图片描述
String str1 = new String("hel") + new String("lo");先创建5个对象:堆中(“hel”,“lo”,“hello”),字符串常量池中(“hel”,“lo”)。str1指向堆。
String str2 = "hello";在常量池中没找到了"abc"的引用,所以创建对象并返回常量池的引用。
String intern = str1.intern();在常量池中找到了"hello"的引用,所以直接返回该引用地址。
str1指向堆的引用,intern 和str2指向的是str1的常量池中的引用,所以str1和str2不相等。

String随着JDK做的改变

JDK1.6:常量池存在于PermGen区(永久代)。缺点:永久代大小不好指定。与Java堆物理隔离,intern()可能产生对个重复的字符串,浪费性能。
JDK1.7:常量池存在于堆中。优点:堆大小不再受于固定大小。位于堆区的常量池,可以被垃圾回收。字符串常量池内部维护一个HashMap,通过需要intern()数量的2倍设置为size(减少hash冲突)。性能更好。
JDK1.8 :存储数据结构char[] 。
JDK1.9 :存储数据结构byte[]。动机:大多数String存的是英文,iso-8859等,只有一个字节就够了(char是两个字节),优化为byte[],如果是中文再用两个字节。

两个问题

大家看到最后应该对字符串常量池及intern方法有了一定的理解。但是这里还有两个问题:
1.在eg3的例子中如果字符串是"java"、"12"、"1122"等一些特殊字符串例子就不成立了。猜测:jvm初始化的时候常量池中就已经存在这些字符串了。
2.通过单测和主线程运行同样代码,结果不一样。查阅后,什么魔法值?

    public static void main(String[] args) {
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//true
    }

    @Test
    public void fun4() {
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//false
    }

到这实在不懂了,渴望分享交流,有兴趣的可以研究一下。

写到最后的感受

从我遇到intern方法想弄懂那刻开始,这期间我查阅了好多,发现言论都不一样,如:字符串常量池中保存的引用还是对象、String str = new String("abc")是创建几个对象、String str = new String("abc") + new String("de")常量池中到底有没有str等。
就这些技术而言,不是很深入的东西一查一大堆,而更加深入一点的问题,就不容易找到答案了。技术的道路是孤独的,这是很平常的事,做技术的人应该有一颗“想要弄懂”的心,然后才能发现和创造。最后,祝大家在技术的路上越走越远,对自己人生追求的事物越走越近!
本文不足的地方,欢迎指正和交流~

本文地址:https://blog.csdn.net/weixin_42476498/article/details/107349701

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网