??本文介紹JVM的本地方法接口和字符串常量池。
??字符串常量池在前文內(nèi)存與垃圾回收|運(yùn)行時(shí)數(shù)據(jù)區(qū)(下)中也有提到,但因其在開(kāi)發(fā)中的使用非常頻繁,所以單獨(dú)來(lái)講。
目錄
?1 本地方法接口
??1.1 本地方法(Native Method)
??1.2 為什么要使用Native Method
??1.3 現(xiàn)狀
?2 字符串常量池(StringTable)
??2.1 2.1 基本特性
??? 2.1.1 String的不可變性
??? 2.1.2 字符串常量池
??2.2 String的內(nèi)存分配
??2.3 字符串拼接操作
??2.4 intern()的使用
??? 2.4.1 intern()效率測(cè)試
??2.5 StrtingTable的垃圾回收
??2.6 G1中的String去重操作
1 本地方法接口
1.1 本地方法(Native Method)
??簡(jiǎn)單來(lái)講,一個(gè)Native Method就是一個(gè)java調(diào)用非java代碼的接口,一個(gè)Native Method 是這樣一個(gè)java方法:該方法的實(shí)現(xiàn)由非Java語(yǔ)言實(shí)現(xiàn),比如C。這個(gè)特征并非java特有,很多其他的編程語(yǔ)言都有這一機(jī)制,比如在C++ 中,你可以用extern “C” 告知C++ 編譯器去調(diào)用一個(gè)C的函數(shù)。
??在定義一個(gè)native method時(shí),并不提供實(shí)現(xiàn)體(有些像定義一個(gè)Java interface),因?yàn)槠鋵?shí)現(xiàn)體是由非java語(yǔ)言在外面實(shí)現(xiàn)的。
??本地接口的作用是融合不同的編程語(yǔ)言為java所用,它的初衷是融合C/C++程序。
/**
* 本地方法 example 01
*/
public class IHaveNatives {
//abstract 沒(méi)有方法體
public abstract void abstractMethod(int x);
//native是有方法體的,但由其他語(yǔ)言來(lái)實(shí)現(xiàn)
//native 和 abstract不能共存
public native void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Native4(int[] array) throws Exception;
}
1.2 為什么要使用Native Method
java使用起來(lái)非常方便,然而有些層次的任務(wù)用java實(shí)現(xiàn)起來(lái)不容易,或者我們對(duì)程序的效率很在意時(shí),問(wèn)題就來(lái)了。
- 與java環(huán)境外交互:
??有時(shí)java應(yīng)用需要與java外面的環(huán)境交互,這是本地方法存在的主要原因。你可以想想java需要與一些底層系統(tǒng),如擦偶偶系統(tǒng)或某些硬件交換信息時(shí)的情況。本地方法正式這樣的一種交流機(jī)制:它為我們提供了一個(gè)非常簡(jiǎn)潔的接口,而且我們無(wú)需去了解java應(yīng)用之外的繁瑣細(xì)節(jié)。 - 與操作系統(tǒng)交互
??JVM支持著java語(yǔ)言本身和運(yùn)行庫(kù),它是java程序賴(lài)以生存的平臺(tái),它由一個(gè)解釋器(解釋字節(jié)碼)和一些連接到本地代碼的庫(kù)組成。然而不管怎樣,它畢竟不是一個(gè)完整的系統(tǒng),它經(jīng)常依賴(lài)于一些底層系統(tǒng)的支持。這些底層系統(tǒng)常常是強(qiáng)大的操作系統(tǒng)。通過(guò)使用本地方法,我們得以用java實(shí)現(xiàn)了jre的與底層系統(tǒng)的交互,甚至jvm的一些部分就是用C寫(xiě)的。還有,如果我們要使用一些java語(yǔ)言本身沒(méi)有提供封裝的操作系統(tǒng)特性時(shí),我們也需要使用本地方法。 - Sun’s Java
??Sun的解釋器是用C實(shí)現(xiàn)的,這使得它能像一些普通的C一樣與外部交互。jre大部分是用java實(shí)現(xiàn)的,它也通過(guò)一些本地方法與外界交互。例如:類(lèi)java.lang.Thread的setPriority()方法是用Java實(shí)現(xiàn)的,但是它實(shí)現(xiàn)調(diào)用的事該類(lèi)里的本地方法setPriority0()。這個(gè)本地方法是用C實(shí)現(xiàn)的,并被植入JVM內(nèi)部,在Windows 95的平臺(tái)上,這個(gè)本地方法最終將調(diào)用Win32 SetProority()API。這是一個(gè)本地方法的具體實(shí)現(xiàn)由JVM直接提供,更多的情況是本地方法由外部的動(dòng)態(tài)鏈接庫(kù)(external dynamic link library)提供,然后被JVM調(diào)用。
1.3 現(xiàn)狀
??目前該方法的是用越來(lái)越少了,除非是與硬件有關(guān)的應(yīng)用,比如通過(guò)java程序驅(qū)動(dòng)打印機(jī)或者java系統(tǒng)管理生產(chǎn)設(shè)備,在企業(yè)級(jí)應(yīng)用已經(jīng)比較少見(jiàn)。因?yàn)楝F(xiàn)在的異構(gòu)領(lǐng)域間的通信很發(fā)達(dá),比如可以使用Socket通信,也可以是用Web Service等等,不多做介紹。
2 字符串常量池(StringTable)

2.1 基本特性
2.1.1 String的不可變性
??String:代表不可變的字符序列。簡(jiǎn)稱(chēng):不可變性。
- 當(dāng)對(duì)字符串重新賦值時(shí),需要重寫(xiě)指定內(nèi)存區(qū)域賦值,不能使用原有的value進(jìn)行賦值。
- 當(dāng)對(duì)現(xiàn)有的字符串進(jìn)行連接操作時(shí),也需要重新指定內(nèi)存區(qū)域賦值,不能使用原有的value進(jìn)行賦值。
- 當(dāng)調(diào)用String的replace()方法修改指定字符或字符串時(shí),也需要重新指定內(nèi)存區(qū)域賦值,不能使用原有的value進(jìn)行賦值。
/**
* String的基本使用:體現(xiàn)String的不可變性 example02
*/
public class StringTest1 {
@Test
public void test1() {
String s1 = "abc";//字面量定義的方式,"abc"存儲(chǔ)在字符串常量池中
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//判斷s1和s2地址: false
System.out.println(s1);//
System.out.println(s2);//abc
}
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);//abcdef
System.out.println(s1);//abc
}
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}
}
/**
* example03 string的不可變性
*/
public class StringExer {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringExer ex = new StringExer();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);//good
System.out.println(ex.ch);//best
}
}
??通過(guò)字面量的方式(區(qū)別于new)給一個(gè)字符串賦值,此時(shí)的字符串值聲明在字符串常量池中。
2.1.2 字符串常量池
??字符串常量池中是不會(huì)存儲(chǔ)相同內(nèi)容的字符串的。
- String的String Pool(又StringTable) 是一個(gè)固定大小的
Hashtable,默認(rèn)值大小長(zhǎng)度是1009。如果放進(jìn)StringPool的String非常多, 就會(huì)造成Hash沖突嚴(yán)重,從而導(dǎo)致鏈表會(huì)很長(zhǎng),而鏈表長(zhǎng)了后直接會(huì)造成的影響就是當(dāng)調(diào)用String. intern時(shí)性能會(huì)大幅下降。 - 使用
-XX:StringTableSize可設(shè)置StringTable的長(zhǎng)度 - 在jdk6中StringTable是固定的,就是1009的長(zhǎng)度,所以如果常量池中的字符串過(guò)多就會(huì)導(dǎo)致效率下降很快。StringTableSize設(shè) 置沒(méi)有要求
- 在jdk7中,StringTable的長(zhǎng)度默認(rèn)值是60013
- jdk8開(kāi)始,1009是StringTable長(zhǎng)度可設(shè)置的最小值
2.2 String的內(nèi)存分配
在Java語(yǔ)言中有8種基本數(shù)據(jù)類(lèi)型和一種比較特殊的類(lèi)型String。這些類(lèi)型為了使它們?cè)谶\(yùn)行過(guò)程中速度更快、更節(jié)省內(nèi)存,都提供了一種常量池的概念。
-
常量池就類(lèi)似一.個(gè)Java系統(tǒng)級(jí)別提供的緩存。8種基本數(shù)據(jù)類(lèi)型的常量
池都是系統(tǒng)協(xié)調(diào)的,String類(lèi) 型的常量池比較特殊。它的主要使用方法有兩種。- 直接使用雙引號(hào)聲明出來(lái)的String對(duì)象會(huì)直接存儲(chǔ)在常量池中。如: String info = "abc" ;
- 如果不是用雙引號(hào)聲明的String對(duì)象,可以使用String提供的intern()方法。這個(gè)后面重點(diǎn)談。
/**
* example04 字面量方式聲明的String會(huì)存儲(chǔ)在常量池中
*/
class Memory {
public static void main(String[] args) {//line 1
int i = 1;//line 2
Object obj = new Object();//line 3
Memory mem = new Memory();//line 4
mem.foo(obj);//line 5
}//line 9
private void foo(Object param) {//line 6
String str = param.toString();//line 7
System.out.println(str);
}//line 8
}

- Java 6及以前,字符串常量池存放在永久代。
- Java 7及以后將字符串常量池的位置調(diào)整到Java堆內(nèi)。
- 所有的字符串都保存在堆(Heap)中,和其他普通對(duì)象一樣,這樣可以讓你在進(jìn)行調(diào)優(yōu)應(yīng)用時(shí)僅需要調(diào)整堆大小就可以了。
StringTable為什么要調(diào)整
①永久代permSize默認(rèn)比較小;
②永久代的垃圾回收頻率低;
2.3 字符串拼接操作
- 常量與常量的拼接結(jié)果在常量池,原理是編譯期優(yōu)化
- 常量池中不會(huì)存在相同內(nèi)容的常量。
- 只要其中有一個(gè)是變量,相當(dāng)于new String對(duì)象,結(jié)果在堆中但不在常量池中。變量拼接的原理是StringBuilder
- 如果拼接的結(jié)果調(diào)用intern()方法,則主動(dòng)將常量池中還沒(méi)有的字符串對(duì)象放入池中,并返回此對(duì)象地址。
/**
* example 05 字符串拼接操作
*/
@Test
public void test1(){
String s1 = "a" + "b" + "c";//編譯期優(yōu)化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,將此地址賦給s2
/*
*.java編譯成.class的結(jié)果
* String s1 = "abc";
* String s2 = "abc"
*/
System.out.println(s1 == s2); //true(比較地址)
System.out.println(s1.equals(s2)); //true
}
@Test
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//編譯期優(yōu)化
//如果拼接符號(hào)的前后出現(xiàn)了變量,則相當(dāng)于在堆空間中new String(),具體的內(nèi)容為拼接的結(jié)果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
//intern():判斷字符串常量池中是否存在javaEEhadoop值,如果存在,則返回常量池中javaEEhadoop的地址;
//如果字符串常量池中不存在javaEEhadoop,則在常量池中加載一份javaEEhadoop,并返回次對(duì)象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8);//true
}
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的s1 + s2 的執(zhí)行細(xì)節(jié):
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 約等于 new String("ab")
補(bǔ)充:在jdk5.0之后使用的是StringBuilder,
在jdk5.0之前使用的是StringBuffer
*/
String s4 = s1 + s2;//
System.out.println(s3 == s4);//false
}
/*
1. 字符串拼接操作不一定使用的是StringBuilder!
如果拼接符號(hào)左右兩邊都是字符串常量或常量引用,則仍然使用編譯期優(yōu)化,即非StringBuilder的方式。
2. 針對(duì)于final修飾類(lèi)、方法、基本數(shù)據(jù)類(lèi)型、引用數(shù)據(jù)類(lèi)型的量的結(jié)構(gòu)時(shí),能使用上final的時(shí)候建議使用上。
*/
@Test
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
//練習(xí):
@Test
public void test5(){
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);//false
final String s4 = "javaEE";//s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);//true
}
2.4 intern()的使用
- 如果不是用雙引號(hào)聲明的String對(duì)象,可以使用String提供的intern方法: intern方法會(huì)從字符串常量池中查詢(xún)當(dāng)前字符串是否存在,若不存在就會(huì)將當(dāng)前字符串放入常量池中。如:
String myInfo = new String("u").intern();
- 也就是說(shuō),如果在任意字符串上調(diào)用String. intern方法,那么其返回結(jié)果所指向的那個(gè)類(lèi)實(shí)例,必須和直接以常量形式出現(xiàn)的字符串實(shí)例完全相同。因此,下列表達(dá)式的值必定是true:
("a" + "b" + "c").intern()== "abc";
通俗點(diǎn)講,Interned String就是確保字符串在內(nèi)存里只有一份拷貝,這樣可以節(jié)約內(nèi)存空間,加快字符串操作任務(wù)的執(zhí)行速度。注意,這個(gè)值會(huì)被存放在字符串內(nèi)部池(String Intern Pool)。
下面有幾個(gè)經(jīng)典題目:
①new String("ab")會(huì)創(chuàng)建幾個(gè)對(duì)象?
一個(gè)對(duì)象是:new關(guān)鍵字在堆空間創(chuàng)建的;另一個(gè)對(duì)象是:字符串常量池中的對(duì)象"ab"。

②new String("a")+new String("b")呢?
* 對(duì)象1:new StringBuilder()
* 對(duì)象2: new String("a")
* 對(duì)象3: 常量池中的"a"
* 對(duì)象4: new String("b")
* 對(duì)象5: 常量池中的"b"
* 對(duì)象6 :new String("ab")

在字符串常量池中,沒(méi)有生成"ab",是因?yàn)閠oString()的調(diào)用,在字符串常量池中,沒(méi)有生成"ab"
③關(guān)于String.intern()的面試題
//examp 06
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
String s1 = s.intern();//調(diào)用此方法之前,字符串常量池中已經(jīng)存在了"1"
String s2 = "1";
//s 指向堆空間"1"的內(nèi)存地址
//s1 指向字符串常量池中"1"的內(nèi)存地址
//s2 指向字符串常量池已存在的"1"的內(nèi)存地址 所以 s1==s2
System.out.println(s == s2);//jdk6:false jdk7/8:false
System.out.println(s1 == s2);//jdk6: true jdk7/8:true
//s3變量記錄的地址為:new String("11")
String s3 = new String("1") + new String("1");
//執(zhí)行完上一行代碼以后,字符串常量池中,不存在"11"!!(上一題中已知)
//在字符串常量池中生成"11"。
//如何理解:jdk6:創(chuàng)建了一個(gè)新的對(duì)象"11",也就有新的地址。
// jdk7:此時(shí)常量中并沒(méi)有創(chuàng)建"11",而是創(chuàng)建一個(gè)指向堆空間中new String("11")的地址
s3.intern();
//s4變量記錄的地址:使用的是上一行代碼代碼執(zhí)行時(shí),在常量池中生成的"11"的地址
String s4 = "11";
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}
④上題拓展
//examp 07
public class StringIntern1 {
public static void main(String[] args) {
//StringIntern.java中練習(xí)的拓展:
String s3 = new String("1") + new String("1");//new String("11")
//執(zhí)行完上一行代碼以后,字符串常量池中,是否存在"11"呢?答案:不存在??!
String s4 = "11";//在字符串常量池中生成對(duì)象"11"
String s5 = s3.intern();
System.out.println(s3 == s4);//false
System.out.println(s5 == s4);//true
}
}
總結(jié)String的intern()的使用
- jdk1.6中,將這個(gè)字符串對(duì)象嘗試放入串池。
? 如果字符串常量池中有,則并不會(huì)放入。返回已有的串池中的對(duì)象的地址
? 如果沒(méi)有,會(huì)把此對(duì)象復(fù)制一份,放入串池,并返回串池中的對(duì)象地址
- Jdk1.7起,將這個(gè)字符串對(duì)象嘗試放入串池。
? 如果字符串常量池中有,則并不會(huì)放入。返回已有的串池中的對(duì)象的地址
? 如果沒(méi)有,則會(huì)把對(duì)象的引用地址復(fù)制一份,放入串池,并返回串池中的引用地址
2.4.1 intern()效率測(cè)試
??大的網(wǎng)站平臺(tái),需要內(nèi)存中存儲(chǔ)大量的字符串。比如社交網(wǎng)站,很多人都存儲(chǔ):北京市、海淀區(qū)等信息。這時(shí)候如果字符串都調(diào)用 intern()方法,就會(huì)明顯降低內(nèi)存的大小。
/**
* 使用intern()測(cè)試執(zhí)行效率:空間使用上
* examp 08
* 結(jié)論:對(duì)于程序中大量存在存在的字符串,尤其其中存在很多重復(fù)字符串時(shí),使用intern()可以節(jié)省內(nèi)存空間。
*
*/
public class StringIntern2 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
// arr[i] = new String(String.valueOf(data[i % data.length]));
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
}
long end = System.currentTimeMillis();
System.out.println("花費(fèi)的時(shí)間為:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
2.5 StrtingTable的垃圾回收
/**
* String的垃圾回收: example 09
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
*/
public class StringGCTest {
public static void main(String[] args) {
// for (int j = 0; j < 100; j++) {
// String.valueOf(j).intern();
// }
//發(fā)生垃圾回收行為
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}

2.6 G1中的String去重操作
-
背景: 對(duì)許多Java應(yīng)用(有大的也有小的)做的測(cè)試得出以下結(jié)果:
- 堆存活數(shù)據(jù)集合里面String對(duì)象占了25%
- 堆存活數(shù)據(jù)集合里面重復(fù)(
string1.equals(string2)=true)的String對(duì)象有13.5% - String對(duì)象的平均長(zhǎng)度是45
-
實(shí)現(xiàn):在G1垃圾收集器中實(shí)現(xiàn)自動(dòng)持續(xù)對(duì)重復(fù)的String對(duì)象進(jìn)行去重,這樣就能避免浪費(fèi)內(nèi)存。
- 當(dāng)垃圾收集器工作的時(shí)候,會(huì)訪問(wèn)堆上存活的對(duì)象。對(duì)每一個(gè)訪問(wèn)的對(duì)象都會(huì)檢查是否是候選的要去重的String對(duì)象。
- 如果是,把這個(gè)對(duì)象的一個(gè)引用插入到隊(duì)列中等待后續(xù)的處理。一個(gè)去重的線程在后臺(tái)運(yùn)行,處理這個(gè)隊(duì)列。處理隊(duì)列的一個(gè)元素意味著從隊(duì)列刪除這個(gè)元素,然后嘗試去重它引用的String對(duì)象。
- 使用一個(gè)hashtable來(lái)記錄所有的被String對(duì)象使用的不重復(fù)的char數(shù)組。
當(dāng)去重的時(shí)候,會(huì)查這個(gè)hashtable,來(lái)看堆上是否已經(jīng)存在一個(gè)一模一樣的char數(shù)組。 - 如果存在,String對(duì)象會(huì)被調(diào)整引用那個(gè)數(shù)組,釋放對(duì)原來(lái)的數(shù)組的引用,最終會(huì)被垃圾收集器回收掉。
- 如果查找失敗,char數(shù)組會(huì)被插入到hashtable,這樣以后的時(shí)候就可以共享這個(gè)數(shù)組了。
-
命令行選項(xiàng)
-
UseStringDeduplication(bool):開(kāi)啟String去重,默認(rèn)是不開(kāi)啟的,需要手動(dòng)開(kāi)啟。 -
PrintStringDedupl icationStatistics(bool):打印詳細(xì)的去重統(tǒng)計(jì)信息, -
StringDedupl icationAgeThreshold (uintx):達(dá)到這個(gè)年齡的string對(duì)象被認(rèn).為是去重的候選對(duì)象
-


