Nacos源码中为什么使用了String.intern方法?

简介: Nacos源码中为什么使用了String.intern方法?

前言

面试的时候经常被问到String的intern方法的调用及内存结构发生的变化。但在实际生产中真正用到过了吗,看到过别人如何使用了吗?

最近阅读Nacos的源码,还真看到代码中使用String类的intern方法,NamingUtils类中有这样一个方法:

public static String getGroupedName(final String serviceName, final String groupName) {
    // ...省略参数校验部分
    final String resultGroupedName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName;
    return resultGroupedName.intern();
}

方法操作很简单,就是拼接一个GrouedName的字符串,但为什么在最后调用了一下intern方法呢?本篇文章我们就来分析一下。

intern方法的基本定义

先来看一下String中intern方法的定义:

public native String intern();

发现是native的方法,暂时我们无法更进一步看到它的具体实现。很多朋友至此便浅尝辄止了,其实我们还可以通过文档说明及一些工具来验证intern方法的作用及运作原理。

在intern方法上有一段注释来介绍它的功能,大意是:当调用intern方法时,如果字符串常量池中不存在对应的字符串(通过equals方法比较),则将该字符串添加到常量池中;如果存在则直接返回对应地址。

我们都知道字符串常量池的功能类似缓存,它可以让程序在运行的过程中速度更快、更节省内存。而上述代码之所以调用intern方法想必便是为了此目的。

字符串及常量池内存结构

要了解intern的作用,不得不先了解一下String字符串的内存结构。

字符串的创建通常有两种形式,通过new关键字创建和通过引号直接赋值的形式。这两种形式的字符串创建在内存分布上是有区别的。

直接使用双引号创建字符串时,会先去常量池查找该字符串是否已经存在,如果不存在的话先在常量池创建常量对象,然后返回引用地址;如果存在,则直接返回。

JDK6及以前的内存结构: image.pngimage.pngPS:JDK8及以后Perm Space改为元空间了,这就不画图展示了。

而使用new关键字创建字符串时,创建的对象是分配在堆中的,栈中的引用指向该对象。

String str2 = new String("hello");

而双引号中的字面值有两种情况,当常量池中不存在字面值“hello”时,会在常量池中生成这样一个常量;如果存在,则堆中的对象直接指向该字面值。

JDK6及以前的内存结构: image.pngimage.png通常面试题中会问到通过new关键字创建String,内存中创建了几个对象,就是基于上面的原理来说的。很显然,如果常量池中已经存在“hello”了,那么只会在堆中创建一个对象,如果常量池中不存在,那就需要现在常量池中存储字符串对象了。因此,答案可能是1个,也可能是2个。

了解了这两个基础的内存逻辑与分布,基本延伸出来的情况(面试题)都可以应答了。

比如:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);//true

两个对象都是直接存放在常量池的,所以引用地址都一样。

再比如:

String s1 = new String("hello");
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2);// false
System.out.println(s1.equals(s2));// true
System.out.println(s1 == s3);//false

其中第一个输出为false是因为s1指向的是堆中的对象地址,s2指向的是常量池的地址;第二个比较的是常量池中存储的字符串,它们共用一个,所以为true;第三个s1和s3虽然共用常量池中的“hello”字面值,但是它们分别在堆中有自己的对象,所以为false。

字符串的拼接

字符串的拼接分两种情况,先看直接加号拼接:

String s1 = "hello" + "word";
String s2 = "helloword";
System.out,println(s1 == s2);//true

这种情况,针对s1,Java编译器是会进行编译期的优化的,编译器会进行字符串的拼接,然后存入常量池的为“helloword”。所以s1和s2都指向常量池中同样的地址。

另外一种情况就是非纯字符串常量的拼接:

String s1 = new String("he") + new String("llo");

针对这种情况,Java编译器同样会进行优化,优化为基于StringBuilder的字符串拼接。

基本流程,先创建一个StringBuilder,然后调用append的方法进行拼接,最后再调用toString方法生成字符串对象。最后通过toString方法生成的这个字符串“hello”,在常量池中是并不存在的。

最终的内存结构为: image.png而最开始讲到的Nacos中的源码,之所以拼接之后调用intern方法的目的就是将上面这种形式拼接的堆中的字符串存储到常量池中。然后直接访问常量池中的对象,从而提升性能。

那么,当String类调用intern之后发生了什么呢?我们下面来看一下。

String的intern()方法

String.intern()方法的功能前面我们已经说过了,下面我们来看一下不同的JDK版本中使用intern方法的效果有何不同。

JDK1.6的实现

在JDK1.6及以前版本中,常量池在永久代分配内存,永久代和Java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用。

如果已经存在该字符串了,则直接返回这个常量池中的这个常量对象的引用。所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生PermGen内存溢出。

String str1 = new String("abc");
String str1Pool = str1.intern();
System.out.println(str1Pool == str1);

上述代码,在JDK1.6中打印结果为false。先看一下内存结构图: image.png在上述代码中,当new String时与前面分析的内存结果一样,会在常量池和堆中创建两个对象。当str1调用intern方法时,发现常量池中已经存在对应的对象了,则该方法返回常量池中对象的地址。此时,str1指向堆中对象地址,str1Pool指向常量池中地址,因此不相等。

还有一种情况是常量池中本来不存在字符串常量:

String str1 = new String("a") + new String("bc");
String str1Pool = str1.intern();
System.out.println(str1Pool == str1);

对应内存结构图如下: image.png上述代码中,字符串str1生成的对象在常量池中并不存在,完全存在于堆中。当然,字符串“a”和“bc”会在创建对象时存入常量池。而当调用intern方法之后,会检查常量池中是否有“abc”,发现没有,于是将“abc”复制到常量池中,intern返回的结果为常量池的地址。此时,很显然,str1Pool和str1一个指向常量池,一个指向堆地址,因此不相等。

但在JDK1.7及以后,事情就发生了变化。

JDK1.7的实现

JDK1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,与之前没有区别。但如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。

简单的说,就是往常量池放的内容变了。原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将堆上的地址引用复制到常量池,也就是常量池存放的是堆中字符串的引用地址。

1.7及以后,常量池已经从方法区中移出来到了堆中。

已经存在的场景我们就不演示了,与JDK1.6一致。下面来看一下常量池不存在对应字符串的情况。

String str1 = new String("a") + new String("bc");
String str1Pool = str1.intern();
System.out.println(str1Pool == str1);

对应的内存结构变化如下: image.png最开始创建“abc”对象时与JDK1.6一样,在堆中创建一个对象,常量池中并不存在“abc”。

当调用intern方法时,常量池不是复制“abc”字面值进行存储,而是直接将堆中“abc”的地址存储在常量池中,并且intern方法返回了堆中对象的地址。

此时会发现str1和str1Pool存储的引用地址都是堆中“abc”的地址。因此上述方法执行的结果为true。

线程池的实现结构

Java使用jni调用c++实现的StringTable的intern方法,StringTable的intern方法跟Java中的HashMap的实现是差不多的,但不能自动扩容,默认大小是1009。

也就是说String的字符串常量池是一个固定大小的Hashtable。如果常量池的String非常多,就会造成Hash冲突严重,导致链表很长,直接后果是会造成当调用String.intern时性能大幅下降。

在JDK1.6中StringTable的长度是固定不变的1009。在JDK1.7中,StringTable的长度可以通过一个参数指定:

-XX:StringTableSize=99991

所以,在使用intern方法时需要慎重。那么,什么场景下适合使用intern方法呢?

就是对应的字符串被大量重复使用的情况下。比如最开始我们讲的Nacos代码,它是服务的名称基本上不会变化,而且会被重复的使用,放在常量池里面就比较合适了。

同时,我们要知道,虽然intern方法可以减少内存占用率,但由于多了一步操作,会导致程序耗时增加。但这与JVM的垃圾回收耗时相比,增加的时间可以忽略不计。

小结

本篇文章的写作的思路纯粹来源于阅读开源框架源码中的一行代码,但如果仔细想一下为什么会如此使用,发掘背后的原理和相关的知识点,也是很有意思的。

目录
相关文章
|
5月前
for循环和String类下方法的一个练习题
for循环和String类下方法的一个练习题
60 1
|
5月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
77 0
|
2月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
81 5
|
3月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
73 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
JavaScript 前端开发 开发者
|
4月前
|
JavaScript 前端开发 API
javaScript中常用的String方法以及注意点总结
本文总结了JavaScript中常用的String对象的方法及其注意事项,包括大小写转换、字符获取、子字符串截取、字符串拼接、去除空格、替换、分割以及查找字符串中字符的索引等操作。提供了每种方法的使用示例代码,帮助理解它们的具体用法和差异。
50 2
|
5月前
|
JavaScript 算法 前端开发
JS算法必备之String常用操作方法
这篇文章详细介绍了JavaScript中字符串的基本操作,包括创建字符串、访问特定字符、字符串的拼接、位置查找、大小写转换、模式匹配、以及字符串的迭代和格式化等方法。
JS算法必备之String常用操作方法
|
5月前
|
XML Java API
List与String相互转化方法汇总
本文汇总了List与String相互转化的多种方法,包括使用`String.join()`、`StringBuilder`、Java 8的Stream API、Apache Commons Lang3的`StringUtils.join()`以及Guava的`Joiner.on()`方法实现List转String;同时介绍了使用`split()`方法、正则表达式、Apache Commons Lang3的`StringUtils.split()`及Guava的`Splitter.on()`方法实现String转List。
171 1
List与String相互转化方法汇总
|
5月前
|
Java API 索引
【Java基础面试二十四】、String类有哪些方法?
这篇文章列举了Java中String类的常用方法,如`charAt()`、`substring()`、`split()`、`trim()`、`indexOf()`、`lastIndexOf()`、`startsWith()`、`endsWith()`、`toUpperCase()`、`toLowerCase()`、`replaceFirst()`和`replaceAll()`,并建议面试时展示对这些方法的熟悉度,同时深入理解部分方法的源码实现。
【Java基础面试二十四】、String类有哪些方法?
|
4月前
|
Java 索引
java基础扫盲-String类常用的方法
java基础扫盲-String类常用的方法