详解Java String中intern方法的原理与使用

 更新时间:2022年05月19日 11:31:38   作者:IT利刃出鞘  
这篇文章主要为大家介绍了Java String中intern方法的原理以及使用。文中通过图片和示例代码进行了详细展示,感兴趣的小伙伴可以跟随小编一起学习一下

简介

本文介绍Java的String的intern方法的原理。

常量池简介

在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池(在方法区)的概念。常量池就类似一个JAVA系统级别提供的缓存。8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。

String的常量池的主要使用方法有两种:

直接使用双引号声明出来的String对象会直接存储在常量池中。

如果不是用双引号声明的String对象,可以使用String提供的intern方法将其放到常量池。

intern方法简介(JDK7)

原型:public native String intern();

说明:

从字符串常量池中查询当前字符串是否存在(通过equals判断)。

  • 如果存在,返回常量池中的字符串引用。
  • 如果不存在,把这个String对象引用存到常量池,然后返回这个String对象的引用。

返回值:都是返回String变量对应的字符串常量池的引用。

示例

package com.example;
 
public class Demo {
    public static void main(String argv[]) {
        String s = "test";
        System.out.println(s == s.intern());
    }
}

JDK6及之前:输出false

JDK7及之后:输出true

原理(JDK6与JDK7)

常量池里的字符串的由来

JDK6及以前调用String.intern()

  • 若常量池中有,则返回常量池中这个字符串的引用
  • 若常量池中没有,则拷贝一份对象,放到常量池(永久代)中;返回值是常量池(永久代)中对应字符串实例的引用。

JDK7及以后调用String.intern()

  • 若常量池中有,则返回常量池中这个字符串的引用
  • 若常量池中没有,则拷贝一份引用,放到常量池(堆)中;(JDK1.7将String常量池从Perm区移动到了Java Heap区)

例程测试

例程1: 

package org.example.a;
 
public class Demo {
    public static void main(String argv[]) {
        String s1 = new String("1");
        s1.intern();
        String s2 = "1";
        System.out.println(s1 == s2);
 
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);
    }
}

结果

jdk6:false false
jdk7:false true
jdk8:false true

例程2: 

package org.example.a;
 
public class Demo {
    public static void main(String argv[]) {
        String s1 = new String("1");
        s1.intern();
        String s2 = "1";
        System.out.println(s1 == s2);
 
        String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);
    }
}

上述代码第二部分有个对调。

结果

jdk6:false false
jdk7:false false
jdk8:false false

例程分析

下边图中:绿色线条代表 String 对象的内容指向。 红色线条代表地址指向。

jdk1.6

例程1与例程2的分析

如上图所示。首先说一下 jdk6中的情况,在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

jdk1.7

在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 会产生java.lang.OutOfMemoryError:PermGen space错误的。在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称jdk8已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在 JAVA的发展了。字符串常量池移动到JAVA Heap区域,现在解释为什么会有上述的打印结果。

例程1的分析

1.String s1 = new String("1"); 

分析:这行代码生成了2个对象(常量池中的“1” 和 JavaHeap 中的字符串对象)。s.intern(); 这一句是 s1 对象去常量池中寻找后发现 “1” 已经在常量池里了。

此时s1指向的是Java Heap中的字符串对象。

2.String s2 = "1"; 

分析:这行代码生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s1 和 s2 的引用地址不同。

3.String s3 = new String("1") + new String("1");

分析:这行代码生成了2个对象(字符串常量池中的“1” 和 Java Heap中的 s3 引用指向的对象“11”(中间还有2个匿名的new String("1")我们不讨论它)。
此时s3 是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是没有 “11”对象的。

4.s3.intern();

分析:这行代码将 s3中的"11"字符串放入String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个"11"的对象,关键点是 jdk7 中常量池不在Perm区域,而是在堆中了。常量池中不需再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。 也就是说引用地址是相同的。

此时,s3是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是有 “11”对象,它保存的就是s3引用地址。

5.String s4 = "11"; 

这行代码”11″是显式声明的,因此会直接去常量池中创建,创建时发现已经有这个对象了。

此时:s4 == 常量池的“11”对象引用 == s3引用对象的引用

例程2的分析

String s1 = new String("1");

s1.intern();

String s2 = "1";

分析:s1.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s1 和 s2 的引用地址是不会相等的。

String s3 = new String("1") + new String("1");

分析:这行代码生成了2个对象(字符串常量池中的“1” 和 Java Heap中的 s3 引用指向的对象“11”(中间还有2个匿名的new String("1")我们不讨论它)。

此时s3 是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是没有 “11”对象的。

String s4 = "11";

分析:声明 s4 的时候常量池中是不存在“11”对象的,执行完后,s4是常量池里“11“对象的引用。

s3.intern(); 

分析:此时常量池中“11”对象已经存在了,不会有任何操作,s3仍然是堆中String对象的引用。因此 s3 != s4

应用实例

package org.example.a;
 
import java.util.Random;
 
public class Demo {
    static final  int MAX = 1000 * 10000;
    static final String[] arr = new String[MAX];
    public static void main(String argv[]) {
        Integer[] DB_DATA = new Integer[10];
        Random random = new Random(10 * 10000);
        for(int i = 0; i < DB_DATA.length; i++){
            DB_DATA[i] = random.nextInt();
        }
 
        long t = System.currentTimeMillis();
        for(int i = 0; i < MAX; i++){
            //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
            arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
        }
 
        System.out.println((System.currentTimeMillis() -t) + "ms");
        System.gc();
    }
}

上述代码是一个演示代码,其中有两条语句不一样,一条是使用 intern,一条是未使用 intern。

运行的参数是:-Xmx2g -Xms2g -Xmn1500M

不用intern

2160ms

使用intern

826ms

通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。

细心的同学会发现使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后, 然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。

到此这篇关于详解Java String中intern方法的原理与使用的文章就介绍到这了,更多相关Java String intern方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring boot2.x中集成H2数据库代码实例

    Spring boot2.x中集成H2数据库代码实例

    这篇文章主要介绍了Spring boot2.x中集成H2数据库代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • 解决StringBuffer和StringBuilder的扩容问题

    解决StringBuffer和StringBuilder的扩容问题

    这篇文章主要介绍了解决StringBuffer和StringBuilder的扩容问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Spring AOP + 注解实现统一注解功能

    Spring AOP + 注解实现统一注解功能

    本文我们通过Spring AOP和Java的自定义注解来实现日志的插入功能,非常不错,具有一定的参考借鉴价值,需要的朋友一起看看吧
    2018-05-05
  • Mybatis 返回值类型和参数传递的配置方法

    Mybatis 返回值类型和参数传递的配置方法

    在 MyBatis 中,返回值类型和参数传递是 Mapper 接口中至关重要的两个方面,正确理解和使用它们可以帮助我们高效、准确地进行数据库操作,接下来通过本文给大家介绍Mybatis 返回值类型和参数传递的配置方法,感兴趣的朋友跟随小编一起看看吧
    2024-08-08
  • 解析Java实现设计模式六大原则之里氏替换原则

    解析Java实现设计模式六大原则之里氏替换原则

    里氏替换原则是用来帮助我们在继承关系中进行父子类的设计。它阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。它是继承复用的基础,反映了基类与子类之间的关系,是对开闭原则的补充,对实现抽象化具体步骤的规范
    2021-06-06
  • 一篇文章带你理解Java的SPI机制(图文并茂)

    一篇文章带你理解Java的SPI机制(图文并茂)

    本文详细介绍了Java的SPI机制,包括其定义、用途和实现方式,SPI(ServiceProviderInterface)是一种服务发现机制,用于实现框架或库的扩展点,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-10-10
  • Java深入讲解Object类常用方法的使用

    Java深入讲解Object类常用方法的使用

    前面java继承中说到的Object类是java中一个特殊的类,所有的类都是直接或者间接的继承自Object类,即如果某个类没有使用extends关键字则默认是java.lang.Object类的子类,所以所有的类都可以使用Object类中定义的方法,下面介绍Object类的常用方法
    2022-04-04
  • AndroidHttpClient使用Cookie应用分析

    AndroidHttpClient使用Cookie应用分析

    今天想把一个用使用了HttpClient的自动签到小程序移植到Android上,还好Android的SDK自带了HttpClient的包.当然也可以继续使用DefaultHttpClient,但用为Android定制的AndroidHttpClient自然更好
    2012-11-11
  • Spring实现文件上传(示例代码)

    Spring实现文件上传(示例代码)

    Spring可以继承commons-fileupload插件来实现文件上传的功能。分为前端JSP编写和后台Controller的编写
    2013-10-10
  • 使用Java通过OAuth协议验证发送微博的教程

    使用Java通过OAuth协议验证发送微博的教程

    这篇文章主要介绍了使用Java通过OAuth协议验证发送微博的教程,使用到了新浪微博为Java开放的API weibo4j,需要的朋友可以参考下
    2016-02-02

最新评论