0.引言

什么都先不说,先看下面这个引入的例子:

  1.  
    String str1 = new String("SEU")+ new String("Calvin");
  2.  
    System.out.println(str1.intern() == str1);
  3.  
    System.out.println(str1 == "SEUCalvin");

本人JDK版本1.8,输出结果为:

  1.  
    true
  2.  
    true

再将上面的例子加上一行代码:

  1.  
    String str2 = "SEUCalvin";//新加的一行代码,其余不变
  2.  
    String str1 = new String("SEU")+ new String("Calvin");
  3.  
    System.out.println(str1.intern() == str1);
  4.  
    System.out.println(str1 == "SEUCalvin");

再运行,结果为:

  1.  
    false
  2.  
    false

是不是感觉莫名其妙,新定义的str2好像和str1没有半毛钱的关系,怎么会影响到有关str1的输出结果呢?其实这都是intern()方法搞的鬼!看完这篇文章,你就会明白。o(∩_∩)o

说实话我本来想总结一篇Android内存泄漏的文章的,查阅了很多资料,发现不得不从Java的OOM讲起,讲Java的OOM又不得不讲Java的虚拟机架构。如果不了解JVM的同学可以查看此篇JVM——Java虚拟机架构。(这篇文章已经被我修改过N多次了,个人感觉还是挺全面清晰的,每次看都会有新的理解。)

在JVM架构一文中也有介绍,在JVM运行时数据区中的方法区有一个常量池,但是发现在JDK1.6以后常量池被放置在了堆空间,因此常量池位置的不同影响到了String的intern()方法的表现。深入了解后发现还是值得写下来记录一下的。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为:SEU_Calvin的博客

1.为什么要介绍intern()方法

intern()方法设计的初衷,就是重用String对象,以节省内存消耗。这么说可能有点抽象,那么就用例子来证明。

  1.  
    static final int MAX = 100000;
  2.  
    static final String[] arr = new String[MAX];
  3.  
     
  4.  
    public static void main(String[] args) throws Exception {
  5.  
    //为长度为10的Integer数组随机赋值
  6.  
    Integer[] sample = new Integer[10];
  7.  
    Random random = new Random(1000);
  8.  
    for (int i = 0; i < sample.length; i++) {
  9.  
    sample[i] = random.nextInt();
  10.  
    }
  11.  
    //记录程序开始时间
  12.  
    long t = System.currentTimeMillis();
  13.  
    //使用/不使用intern方法为10万个String赋值,值来自于Integer数组的10个数
  14.  
    for (int i = 0; i < MAX; i++) {
  15.  
    arr[i] = new String(String.valueOf(sample[i % sample.length]));
  16.  
    //arr[i] = new String(String.valueOf(sample[i % sample.length])).intern();
  17.  
    }
  18.  
    System.out.println((System.currentTimeMillis() - t) + "ms");
  19.  
    System.gc();
  20.  
    }

这个例子也比较简单,就是为了证明使用intern()比不使用intern()消耗的内存更少。

先定义一个长度为10的Integer数组,并随机为其赋值,在通过for循环为长度为10万的String对象依次赋值,这些值都来自于Integer数组。两种情况分别运行,可通过Window ---> Preferences --> Java --> Installed JREs设置JVM启动参数为-agentlib:hprof=heap=dump,format=b,将程序运行完后的hprof置于工程目录下。再通过MAT插件查看该hprof文件。
两次实验结果如下:

从运行结果来看,不使用intern()的情况下,程序生成了101762个String对象,而使用了intern()方法时,程序仅生成了1772个String对象。自然也证明了intern()节省内存的结论。

细心的同学会发现使用了intern()方法后程序运行时间有所增加。这是因为程序中每次都是用了new String后又进行intern()操作的耗时时间,但是不使用intern()占用内存空间导致GC的时间是要远远大于这点时间的。

2.深入认识intern()方法

JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子:

  1.  
    String s = new String("1");
  2.  
    s.intern();
  3.  
    String s2 = "1";
  4.  
    System.out.println(s == s2);
  5.  
     
  6.  
    String s3 = new String("1") + new String("1");
  7.  
    s3.intern();
  8.  
    String s4 = "11";
  9.  
    System.out.println(s3 == s4);

输出结果为:

  1.  
    JDK1.6以及以下:false false
  2.  
    JDK1.7以及以上:false true

再分别调整上面代码2.3行、7.8行的顺序:

  1.  
    String s = new String("1");
  2.  
    String s2 = "1";
  3.  
    s.intern();
  4.  
    System.out.println(s == s2);
  5.  
     
  6.  
    String s3 = new String("1") + new String("1");
  7.  
    String s4 = "11";
  8.  
    s3.intern();
  9.  
    System.out.println(s3 == s4);

输出结果为:

  1.  
    JDK1.6以及以下:false false
  2.  
    JDK1.7以及以上:false false

下面依据上面代码对intern()方法进行分析:

2.1 JDK1.6

在JDK1.6中所有的输出结果都是 false,因为JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,熟悉JVM的话应该知道这是和堆区完全分开的。

使用引号声明的字符串都是会直接在字符串常量池中生成的,而 new 出来的 String 对象是放在堆空间中的。所以两者的内存地址肯定是不相同的,即使调用了intern()方法也是不影响的。如果不清楚String类的“==”和equals()的区别可以查看我的这篇博文Java面试——从Java堆、栈角度比较equals和==的区别

intern()方法在JDK1.6中的作用是:比如String s = new String("SEU_Calvin"),再调用s.intern(),此时返回值还是字符串"SEU_Calvin",表面上看起来好像这个方法没什么用处。但实际上,在JDK1.6中它做了个小动作:检查字符串池里是否存在"SEU_Calvin"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"SEU_Calvin"添加到字符串池中,然后再返回它的引用。然而在JDK1.7中却不是这样的,后面会讨论。

2.2 JDK1.7

针对JDK1.7以及以上的版本,我们将上面两段代码分开讨论。先看第一段代码的情况:

再把第一段代码贴一下便于查看:

  1.  
    String s = new String("1");
  2.  
    s.intern();
  3.  
    String s2 = "1";
  4.  
    System.out.println(s == s2);
  5.  
     
  6.  
    String s3 = new String("1") + new String("1");
  7.  
    s3.intern();
  8.  
    String s4 = "11";
  9.  
    System.out.println(s3 == s4);

String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。

s.intern(),这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。

String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象。

结果就是 s 和 s2 的引用地址明显不同。因此返回了false。

String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。

s3.intern(),这一行代码,是将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一个 "11" 的对象。

但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。

String s4 = "11", 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。

下面继续分析第二段代码:

再把第二段代码贴一下便于查看:

  1.  
    String s = new String("1");
  2.  
    String s2 = "1";
  3.  
    s.intern();
  4.  
    System.out.println(s == s2);
  5.  
     
  6.  
    String s3 = new String("1") + new String("1");
  7.  
    String s4 = "11";
  8.  
    s3.intern();
  9.  
    System.out.println(s3 == s4);

String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。

String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象,但是发现已经存在了,那么就直接指向了它。

s.intern(),这一行在这里就没什么实际作用了。因为"1"已经存在了。

结果就是 s 和 s2 的引用地址明显不同。因此返回了false。

String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。

String s4 = "11", 这一行代码会直接去生成常量池中的"11"。

s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。

结果就是 s3 和 s4 的引用地址明显不同。因此返回了false。

为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为:SEU_Calvin的博客

3 总结

终于要做Ending了。现在再来看一下开篇给的引入例子,是不是就很清晰了呢。

  1.  
    String str1 = new String("SEU") + new String("Calvin");
  2.  
    System.out.println(str1.intern() == str1);
  3.  
    System.out.println(str1 == "SEUCalvin");

str1.intern() == str1就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,因此指向了str1。 "SEUCalvin"在常量池中创建时,也就直接指向了str1了。两个都返回true就理所当然啦。

那么第二段代码呢:

  1.  
    String str2 = "SEUCalvin";//新加的一行代码,其余不变
  2.  
    String str1 = new String("SEU")+ new String("Calvin");
  3.  
    System.out.println(str1.intern() == str1);
  4.  
    System.out.println(str1 == "SEUCalvin");

也很简单啦,str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,你可以去验证它们两个是返回的true。后面的"SEUCalvin"也一样指向str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。

好了,本篇对intern的作用以及在JDK1.6和1.7中的实现原理的介绍就到此为止了。希望能给你带来帮助。

Java技术——你真的了解String类的intern()方法吗的更多相关文章

  1. JVM体系结构之七:持久代、元空间(Metaspace) 常量池==了解String类的intern()方法、常量池介绍、常量池从Perm-->Heap

    一.intern()定义及使用 相信绝大多数的人不会去用String类的intern方法,打开String类的源码发现这是一个本地方法,定义如下: public native String inter ...

  2. String类中intern方法的原理分析

    一,前言 ​ 昨天简单整理了JVM内存分配和String类常用方法,遇到了String中的intern()方法.本来想一并总结起来,但是intern方法还涉及到JDK版本的问题,内容也相对较多,所以今 ...

  3. Java String类的intern()方法

    该方法的作用是把字符串加载到常量池中(jdk1.6常量池位于方法区,jdk1.7以后常量池位于堆) 在jdk1.6中,该方法把字符串的值复制到常量区,然后返回常量区里这个字符串的值: 在jdk1.7里 ...

  4. JAVA中String类的intern()方法的作用

    一般我们变成很少使用到 intern这个方法,今天我就来解释一下这个方法是干什么的,做什么用的 首先请大家看一个例子: public static void main(String[] args) t ...

  5. String类的intern()方法

    0.引言 什么都先不说,先看下面这个引入的例子: String str1 = new String("SEU")+ new String("Calvin"); ...

  6. String类的intern()方法,随常量池发生的变化

    JVM的知识这里总结的很详细:https://github.com/doocs/jvm/blob/master/README.md,因此在本博客也不会再对其中的东西重复总结了. intern的作用 简 ...

  7. java.lang.String 类的所有方法

    java.lang.String 类的所有方法 方法摘要 char charAt(int index) 返回指定索引处的 char 值. int codePointAt(int index) 返回指定 ...

  8. Java中String类的format方法使用总结

    可参考: http://www.cnblogs.com/fsjohnhuang/p/4094777.html http://kgd1120.iteye.com/blog/1293633 String类 ...

  9. Java字符串的匹配问题,String类的matches方法与Matcher类的matches方法的使用比较,Matcher类的matches()、find()和lookingAt()方法的使用比较

    参考网上相关blog,对Java字符串的匹配问题进行了简单的比较和总结,主要对String类的matches方法与Matcher类的matches方法进行了比较. 对Matcher类的matches( ...

随机推荐

  1. IP代理

    import requests # 定义爬取url地址 base_url = 'https://www.baidu.com/' # 定义代理IP地址 proxies = {'http':'http:/ ...

  2. 容器学习笔记之CentOS7安装Docker(安装指定版本的Docker,加速,卸载)

    0x00 概述 Docker从1.13版本之后采用时间线的方式作为版本号,分为社区版CE和企业版EE. 社区版是免费提供给个人开发者和小型团体使用的,企业版会提供额外的收费服务,比如经过官方测试认证过 ...

  3. 树莓派无显示器开启ssh的方法

    在boot根目录新建一个名为 ssh 的空文件即可. boot目录所在分区是fat32格式,可以被windows识别和操作 带有系统的tf卡(或SD卡)插入读卡器中,新建ssh文件即可,注意无后缀名

  4. The POM for XXX is invalid, transitive dependencies (if any) will not be available解决方案

    今天,某个开发的环境在编译的时候提示警告The POM for XXX is invalid, transitive dependencies (if any) will not be availab ...

  5. MVC HTML页面使用

    解决HTML <system.webServer> <validation validateIntegratedModeConfiguration="false" ...

  6. jsxyhelu的GitHub使用方法

    如果只是使用Clone不能称得上是完全使用了GitHub,必须完成PullRequest,而且最好是对大型.带自动构建项目进行PR(比如OpenCV),这样才叫完全掌握GitHub的使用方法,这里分享 ...

  7. Exp2_固件程序设计 20165226_20165310_20165315

    Exp2_固件程序设计 20165226_20165310_20165315 Exp2_1 MDK 实验内容 注意不经老师允许不准烧写自己修改的代码 两人(个别三人)一组 参考云班课资源中" ...

  8. 20145325张梓靖 《网络对抗技术》 Web安全基础实践

    20145325张梓靖 <网络对抗技术> Web安全基础实践 实验内容 使用webgoat进行XSS攻击.CSRF攻击.SQL注入 XSS攻击:Stored XSS Attacks.Ref ...

  9. Codeforces Round #479 (Div. 3)题解

    CF首次推出div3给我这种辣鸡做,当然得写份博客纪念下 A. Wrong Subtraction time limit per test 1 second memory limit per test ...

  10. Eclipse git commit错误;Committing changes has encountered a problem An Internal error occured

    背景 在使用eclipse时,使用git commit 提交代码时,出项如下错误 解决方法 在工程目录下找到 .git 文件夹 ,找到里面的 index.lock 文件,然后删掉这个文件就可以了,如下 ...