日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
Nacos源碼中為什么使用了String.intern方法?

本文轉(zhuǎn)載自微信公眾號(hào)「程序新視界」,作者二師兄。轉(zhuǎn)載本文請(qǐng)聯(lián)系程序新視界公眾號(hào)。

前言

面試的時(shí)候經(jīng)常被問到String的intern方法的調(diào)用及內(nèi)存結(jié)構(gòu)發(fā)生的變化。但在實(shí)際生產(chǎn)中真正用到過了嗎,看到過別人如何使用了嗎?

最近閱讀Nacos的源碼,還真看到代碼中使用String類的intern方法,NamingUtils類中有這樣一個(gè)方法:

 
 
 
 
  1. public static String getGroupedName(final String serviceName, final String groupName) { 
  2.     // ...省略參數(shù)校驗(yàn)部分 
  3.     final String resultGroupedName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName; 
  4.     return resultGroupedName.intern(); 

方法操作很簡(jiǎn)單,就是拼接一個(gè)GrouedName的字符串,但為什么在最后調(diào)用了一下intern方法呢?本篇文章我們就來分析一下。

intern方法的基本定義

先來看一下String中intern方法的定義:

 
 
 
 
  1. public native String intern(); 

發(fā)現(xiàn)是native的方法,暫時(shí)我們無法更進(jìn)一步看到它的具體實(shí)現(xiàn)。很多朋友至此便淺嘗輒止了,其實(shí)我們還可以通過文檔說明及一些工具來驗(yàn)證intern方法的作用及運(yùn)作原理。

在intern方法上有一段注釋來介紹它的功能,大意是:當(dāng)調(diào)用intern方法時(shí),如果字符串常量池中不存在對(duì)應(yīng)的字符串(通過equals方法比較),則將該字符串添加到常量池中;如果存在則直接返回對(duì)應(yīng)地址。

我們都知道字符串常量池的功能類似緩存,它可以讓程序在運(yùn)行的過程中速度更快、更節(jié)省內(nèi)存。而上述代碼之所以調(diào)用intern方法想必便是為了此目的。

字符串及常量池內(nèi)存結(jié)構(gòu)

要了解intern的作用,不得不先了解一下String字符串的內(nèi)存結(jié)構(gòu)。

字符串的創(chuàng)建通常有兩種形式,通過new關(guān)鍵字創(chuàng)建和通過引號(hào)直接賦值的形式。這兩種形式的字符串創(chuàng)建在內(nèi)存分布上是有區(qū)別的。

直接使用雙引號(hào)創(chuàng)建字符串時(shí),會(huì)先去常量池查找該字符串是否已經(jīng)存在,如果不存在的話先在常量池創(chuàng)建常量對(duì)象,然后返回引用地址;如果存在,則直接返回。

JDK6及以前的內(nèi)存結(jié)構(gòu):

JDK7及以后的內(nèi)存結(jié)構(gòu):

PS:JDK8及以后Perm Space改為元空間了,這就不畫圖展示了。

而使用new關(guān)鍵字創(chuàng)建字符串時(shí),創(chuàng)建的對(duì)象是分配在堆中的,棧中的引用指向該對(duì)象。

 
 
 
 
  1. String str2 = new String("hello"); 

而雙引號(hào)中的字面值有兩種情況,當(dāng)常量池中不存在字面值“hello”時(shí),會(huì)在常量池中生成這樣一個(gè)常量;如果存在,則堆中的對(duì)象直接指向該字面值。

JDK6及以前的內(nèi)存結(jié)構(gòu):

JDK7及以后的內(nèi)存結(jié)構(gòu):

通常面試題中會(huì)問到通過new關(guān)鍵字創(chuàng)建String,內(nèi)存中創(chuàng)建了幾個(gè)對(duì)象,就是基于上面的原理來說的。很顯然,如果常量池中已經(jīng)存在“hello”了,那么只會(huì)在堆中創(chuàng)建一個(gè)對(duì)象,如果常量池中不存在,那就需要現(xiàn)在常量池中存儲(chǔ)字符串對(duì)象了。因此,答案可能是1個(gè),也可能是2個(gè)。

了解了這兩個(gè)基礎(chǔ)的內(nèi)存邏輯與分布,基本延伸出來的情況(面試題)都可以應(yīng)答了。

比如:

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

兩個(gè)對(duì)象都是直接存放在常量池的,所以引用地址都一樣。

再比如:

 
 
 
 
  1. String s1 = new String("hello"); 
  2. String s2 = "hello"; 
  3. String s3 = new String("hello"); 
  4.  
  5. System.out.println(s1 == s2);// false 
  6. System.out.println(s1.equals(s2));// true 
  7. System.out.println(s1 == s3);//false 

其中第一個(gè)輸出為false是因?yàn)閟1指向的是堆中的對(duì)象地址,s2指向的是常量池的地址;第二個(gè)比較的是常量池中存儲(chǔ)的字符串,它們共用一個(gè),所以為true;第三個(gè)s1和s3雖然共用常量池中的“hello”字面值,但是它們分別在堆中有自己的對(duì)象,所以為false。

字符串的拼接

字符串的拼接分兩種情況,先看直接加號(hào)拼接:

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

這種情況,針對(duì)s1,Java編譯器是會(huì)進(jìn)行編譯期的優(yōu)化的,編譯器會(huì)進(jìn)行字符串的拼接,然后存入常量池的為“helloword”。所以s1和s2都指向常量池中同樣的地址。

另外一種情況就是非純字符串常量的拼接:

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

針對(duì)這種情況,Java編譯器同樣會(huì)進(jìn)行優(yōu)化,優(yōu)化為基于StringBuilder的字符串拼接。

基本流程,先創(chuàng)建一個(gè)StringBuilder,然后調(diào)用append的方法進(jìn)行拼接,最后再調(diào)用toString方法生成字符串對(duì)象。最后通過toString方法生成的這個(gè)字符串“hello”,在常量池中是并不存在的。

最終的內(nèi)存結(jié)構(gòu)為:

而最開始講到的Nacos中的源碼,之所以拼接之后調(diào)用intern方法的目的就是將上面這種形式拼接的堆中的字符串存儲(chǔ)到常量池中。然后直接訪問常量池中的對(duì)象,從而提升性能。

那么,當(dāng)String類調(diào)用intern之后發(fā)生了什么呢?我們下面來看一下。

String的intern()方法

String.intern()方法的功能前面我們已經(jīng)說過了,下面我們來看一下不同的JDK版本中使用intern方法的效果有何不同。

JDK1.6的實(shí)現(xiàn)

在JDK1.6及以前版本中,常量池在永久代分配內(nèi)存,永久代和Java堆的內(nèi)存是物理隔離的,執(zhí)行intern方法時(shí),如果常量池不存在該字符串,虛擬機(jī)會(huì)在常量池中復(fù)制該字符串,并返回引用。

如果已經(jīng)存在該字符串了,則直接返回這個(gè)常量池中的這個(gè)常量對(duì)象的引用。所以需要謹(jǐn)慎使用intern方法,避免常量池中字符串過多,導(dǎo)致性能變慢,甚至發(fā)生PermGen內(nèi)存溢出。

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

上述代碼,在JDK1.6中打印結(jié)果為false。先看一下內(nèi)存結(jié)構(gòu)圖:

在上述代碼中,當(dāng)new String時(shí)與前面分析的內(nèi)存結(jié)果一樣,會(huì)在常量池和堆中創(chuàng)建兩個(gè)對(duì)象。當(dāng)str1調(diào)用intern方法時(shí),發(fā)現(xiàn)常量池中已經(jīng)存在對(duì)應(yīng)的對(duì)象了,則該方法返回常量池中對(duì)象的地址。此時(shí),str1指向堆中對(duì)象地址,str1Pool指向常量池中地址,因此不相等。

還有一種情況是常量池中本來不存在字符串常量:

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

對(duì)應(yīng)內(nèi)存結(jié)構(gòu)圖如下:

上述代碼中,字符串str1生成的對(duì)象在常量池中并不存在,完全存在于堆中。當(dāng)然,字符串“a”和“bc”會(huì)在創(chuàng)建對(duì)象時(shí)存入常量池。而當(dāng)調(diào)用intern方法之后,會(huì)檢查常量池中是否有“abc”,發(fā)現(xiàn)沒有,于是將“abc”復(fù)制到常量池中,intern返回的結(jié)果為常量池的地址。此時(shí),很顯然,str1Pool和str1一個(gè)指向常量池,一個(gè)指向堆地址,因此不相等。

但在JDK1.7及以后,事情就發(fā)生了變化。

JDK1.7的實(shí)現(xiàn)

JDK1.7后,intern方法還是會(huì)先去查詢常量池中是否有已經(jīng)存在,如果存在,則返回常量池中的引用,與之前沒有區(qū)別。但如果在常量池找不到對(duì)應(yīng)的字符串,則不會(huì)再將字符串拷貝到常量池,而只是在常量池中生成一個(gè)對(duì)原字符串的引用。

簡(jiǎn)單的說,就是往常量池放的內(nèi)容變了。原來在常量池中找不到時(shí),復(fù)制一個(gè)副本放到常量池,1.7后則是將堆上的地址引用復(fù)制到常量池,也就是常量池存放的是堆中字符串的引用地址。

1.7及以后,常量池已經(jīng)從方法區(qū)中移出來到了堆中。

已經(jīng)存在的場(chǎng)景我們就不演示了,與JDK1.6一致。下面來看一下常量池不存在對(duì)應(yīng)字符串的情況。

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

對(duì)應(yīng)的內(nèi)存結(jié)構(gòu)變化如下:

最開始創(chuàng)建“abc”對(duì)象時(shí)與JDK1.6一樣,在堆中創(chuàng)建一個(gè)對(duì)象,常量池中并不存在“abc”。

當(dāng)調(diào)用intern方法時(shí),常量池不是復(fù)制“abc”字面值進(jìn)行存儲(chǔ),而是直接將堆中“abc”的地址存儲(chǔ)在常量池中,并且intern方法返回了堆中對(duì)象的地址。

此時(shí)會(huì)發(fā)現(xiàn)str1和str1Pool存儲(chǔ)的引用地址都是堆中“abc”的地址。因此上述方法執(zhí)行的結(jié)果為true。

線程池的實(shí)現(xiàn)結(jié)構(gòu)

Java使用jni調(diào)用c++實(shí)現(xiàn)的StringTable的intern方法,StringTable的intern方法跟Java中的HashMap的實(shí)現(xiàn)是差不多的,但不能自動(dòng)擴(kuò)容,默認(rèn)大小是1009。

也就是說String的字符串常量池是一個(gè)固定大小的Hashtable。如果常量池的String非常多,就會(huì)造成Hash沖突嚴(yán)重,導(dǎo)致鏈表很長(zhǎng),直接后果是會(huì)造成當(dāng)調(diào)用String.intern時(shí)性能大幅下降。

在JDK1.6中StringTable的長(zhǎng)度是固定不變的1009。在JDK1.7中,StringTable的長(zhǎng)度可以通過一個(gè)參數(shù)指定:

 
 
 
 
  1. -XX:StringTableSize=99991 

所以,在使用intern方法時(shí)需要慎重。那么,什么場(chǎng)景下適合使用intern方法呢?

就是對(duì)應(yīng)的字符串被大量重復(fù)使用的情況下。比如最開始我們講的Nacos代碼,它是服務(wù)的名稱基本上不會(huì)變化,而且會(huì)被重復(fù)的使用,放在常量池里面就比較合適了。

同時(shí),我們要知道,雖然intern方法可以減少內(nèi)存占用率,但由于多了一步操作,會(huì)導(dǎo)致程序耗時(shí)增加。但這與JVM的垃圾回收耗時(shí)相比,增加的時(shí)間可以忽略不計(jì)。

小結(jié)

本篇文章的寫作的思路純粹來源于閱讀開源框架源碼中的一行代碼,但如果仔細(xì)想一下為什么會(huì)如此使用,發(fā)掘背后的原理和相關(guān)的知識(shí)點(diǎn),也是很有意思的。


網(wǎng)站欄目:Nacos源碼中為什么使用了String.intern方法?
文章轉(zhuǎn)載:http://www.dlmjj.cn/article/djppgeg.html