前言
咱们在理论开发当中应用String十分的宽泛,那么对应用String类其实有很多角度能够去学习了解
那么本篇文章,咱们从应用String的档次到开始理解剖析String的实现、性能等等
一、String的根本个性
对于String咱们称为字符串,应用一对 “” 引号
起来示意
那么平时咱们的应用有不同的定义形式如下:
String s1 = "xiaomingtongxue" ;
称说为字面量的定义形式String s2 = new String("hello");
称说为对象的形式
咱们能够察看一下String的源代码剖析看看
如图能够察看到String被申明为final的:示意它不可被继承
如图能够察看到String实现了Serializable接口:示意字符串是反对序列化的
如图能够察看到实现了Comparable接口:示意String能够比拟大小
如图能够察看到String在jdk8及以前外部定义了final char value[]
用于存储字符串数据
然而它在JDK9时改为了byte[],咱们能够切换到JDK9的环境去看看String的源码
为什么 JDK9 扭转了 String 的构造
================================
能够拜访官网文档查看具体的阐明:拜访入口
具体咱们就粘贴官网的阐明进行翻译解释
论断:String再也不必char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了批改
对于String来说它代表了不可变的字符序列,简称:不可变性。
接下来咱们应用示例领会一下它的不可变性
public void test2() {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
//运行如下:
true
abc
abc
论断:这两个abc理论共用的是堆空间里的字符串常量池里边的同一个,所以两个援用地址是一样的
public void test2() {
String s1 = "abc";
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
//运行如下:
false
abc
abc
论断:当对s1字符串从新赋值时会新建一个指定内存区域并赋值,所以在比拟的时候两个援用地址不一样
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);
System.out.println(s1);
}
//运行如下:
abcdef
abc
论断:当对s1字符串进行连贯操作时也会新建一个指定内存区域并赋值,不在原有的value进行赋值
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);
System.out.println(s2);
}
//运行如下:
abc
mbc
论断:当调用string的replace()操作时也会新建一个指定内存区域并赋值,不在原有的value进行赋值
一道口试题
================================
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);
System.out.println(ex.ch);
}
}
那么当咱们运行起来的时候,会输入什么呢?输入:good、best
原 str 的援用地址的内容并没有变,办法里的str = “test ok” ,其实是字符串常量池中的另一个区域(地址),将它进行赋值操作给str但并没有批改原来 str 指向的援用地址里的内容
论断:字符串常量池中是不会存储雷同内容的字符串的
。
字符串常量池怎么保障不会存储雷同内容的?
================================
因为String Pool(字符串常量池)是一个固定大小的Hashtable,默认值大小长度是1009
在JDK6
中StringTable是固定的就是1009的长度
,所以如果常量池中的字符串过多就会导致效率降落很快,StringTablesize设置没有要求
在JDK7
中,StringTable的长度默认值是60013
在JDK8
中,StringTable的长度默认值是60013
,StringTable能够设置的最小值为1009
如果放进String Pool的String十分多就会造成Hash抵触重大,从而导致链表会很长而链表长了后间接会造成的影响就是当调用String.intern()办法时性能会大幅降落
所以裁减StringTable的长度,应用-XX:StringTablesize
可设置StringTable的长度
咱们应用一个示例来领会一下不同版本的默认长度是多少
public static void main(String[] args) {
//测试StringTablesize参数
System.out.println("我来打个酱油");
try {
Thread.sleep(1000000);
}catch (InterruptedException e){
e. printStackTrace();
}
}
JDK 6环境下的大小设置:
这时将我的项目跑起来,在关上cmd命令窗口查看一下是否已被批改为:10的大小
JDK 7环境下的大小设置:
这时将我的项目跑起来,在关上cmd命令窗口查看一下大小是多少
接下来咱们再测试不同大小长度的速度是怎么样的,学生成10万个长度不超过10的字符串
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("words.txt");
for (int i = 0; i < 100000; i++) {
//1 - 10
int length = (int)(Math.random() * (10 - 1 + 1) + 1);
fw.write(getString(length) + "\n");
}
fw.close();
}
public static String getString(int length){
String str = "";
for (int i = 0; i < length; i++) {
//65 - 90, 97-122
int num = (int)(Math.random() * (90 - 65 + 1) + 65) + (int)(Math.random() * 2) * 32;
str += (char)num;
}
return str;
}
接下来咱们依据设置不同大小看看,将这10万个长度不超过10的字符串读取看看效率怎么样
破费工夫:143ms
破费工夫:47ms
二、String的内存调配
那么String构造次要放在哪呢?其实咱们在前几篇有提到过这个放的地位
那么咱们再出于完整性的思考并且例子来阐明的确是这样的构造当中
首先咱们说在Java语言中有8种根本数据类型
和1种比拟非凡的类型String
那么这九种那个唯独能够放在一起呢?那就是常量池使得它们更快更节俭内存等
常量池就相似一个Java零碎级别提供的缓存,8种根本数据类型的常量池都是零碎协调的,String类型的常量池比拟非凡。它的次要应用办法有两种
- 间接应用双引号申明进去的String对象会间接存储在常量池中。
- 如果不是用双引号申明的String对象,能够应用String提供的intern()办法。这
Java 6 及以前,字符串常量池寄存在永恒代
Java 7中 Oracle的工程师对字符串池的逻辑做了很大的扭转,行将字符串常量池的地位调整到Java堆内
所有的字符串都保留在堆(Heap)中,和其余一般对象一样,这样能够让你在进行调优利用时仅须要调整堆大小就能够了
字符串常量池概念本来应用得比拟多,然而这个改变使得咱们有足够的理由让咱们重新考虑在Java 7中应用String.intern()。
Java8元空间,字符串常量在堆
咱们能够应用示例来领会一下字符串常量池爆出OOM的不同状况
public class StringTest3 {
public static void main(String[] args) {
//应用Set放弃着常量池援用,防止full gc回收常量池行为
Set<String> set = new HashSet<String>();
//在short能够取值的范畴内足以让6MB的PermSize或heap产生OOM了。
short i = 0;
while(true){
set.add(String.valueOf(i++).intern());
}
}
}
JDK 6环境运行下在永恒代中:
Exception in thread "main" java.lang.outOfMemoryError: PermGen space
at java.lang.string.intern(Native Method)
at com.atguigu.java.stringTest3.main(StringTest3. java:22)
JDK 8环境运行下在堆中:
运行后果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:703)
at java.util.HashMap.putVal(HashMap.java:662)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at com.atguigu.java.StringTest3.main(StringTest3.java:22)
那么为什么要调整字符串常量池的地位?
================================
首先永恒代的默认空间大小比拟小并且垃圾回收频率低,大量的字符串无奈及时回收,容易进行Full GC产生STW或者容易产生OOM:PermGen Space
而堆中空间足够大,字符串可被及时回收
在JDK 7中,interned字符串不再在Java堆的永恒代中调配,而是在Java堆的次要局部(称为年老代和年轻代)中调配,与应用程序创立的其余对象一起调配
此更改将导致驻留在主Java堆中的数据更多,驻留在永恒生成中的数据更少,因而可能须要调整堆大小
三、String的基本操作
咱们来看一个代码示例,依照咱们之前说的个性,前面同样的字符串则不会再生成
public static void main(String[] args) {
System.out.println();
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
//如下的字符串"1" 到 "10"不会再次加载
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
}
那么咱们应用debug运行起来代码,看看真的是这样吗?
Java语言标准里要求完全相同的字符串字面量,应该蕴含同样的Unicode字符序列(蕴含同一份码点序列的常量),并且必须是指向同一个String类实例
那么接下来咱们再看一个列子,当咱们运行起来一起剖析main办法与foo办法各指向的地位
//官网示例代码
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
}
四、字符串拼接操作
咱们先来看看以下代码的拼接操作,若进行匹配的话后果是什么呢?
public void test1(){
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
后果是输入两个true、为什么呢?咱们先将它进行编译一起看看.class文件内容是什么
public void test1(){
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
咱们发现常量与常量的拼接的话,它们的后果在常量池原理是编译优化将s1等同于”abc”
同时咱们也能够看看该字节码是怎么回事
从字节码指令看出:编译器做了优化,将 “a” + “b” + “c” 优化成了 “abc”
接下来在看看下一个示例代码
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
String s8 = s6.intern();
System.out.println(s3 == s8);
}
那么当代码运行起来后运行后果为:true、false、false、false、false、false、false、false、
那么为什么会这样呢?咱们和下面一样将它编译并且看看.class文件内容
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEEhadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
String s8 = s6.intern();
System.out.println(s3 == s8);
}
咱们发现s4 常量与常量的拼接的话,会进行编译优化就连接成一起了
如果拼接符号的前后呈现了变量则相当于在堆空间中new String(),具体的内容为拼接的后果:javaEEhadoop
(s5、s6、s7呈现)所以此时再进行比拟的时候,后果就会为false
那么咱们发现s8 = s6.intern(),那么它是什么状况呢?
如果拼接的后果调用intern()办法,依据该字符串是否在常量池中存在,分为:
- 如果存在,则返回字符串在常量池中的地址
- 如果字符串常量池中不存在该字符串,则在常量池中创立一份,并返回此对象的地址
而咱们s6 拼接符号的前前面呈现了变量则堆空间中new String(),所以字符串常量池不存在
接下来在看看下一个示例代码
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
那么当代码运行起来后运行后果为:false
那么为什么会这样呢?咱们一起运行剖析一下字节码看看
咱们发现s4 = s1 + s2 的时候第九行操作指令创立StringBuilder并进行了实例化赋默认值
将局部变量表索引为1的a、索引为2的ab进行append。咱们能够应用长期代码展现这个操作
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
/*
如下的s1 + s2 的执行细节:(变量s是我长期定义的)
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 约等于 new String("ab"),但不等价
补充:在jdk5.0之后应用的是StringBuilder,在jdk5.0之前应用的是StringBuffer
*/
System.out.println(s3 == s4);
}
论断:拼接前后只有其中有一个是变量后果就在堆中。变量拼接的原理是StringBuilder
接下来在看看下一个示例代码
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
那么当代码运行起来后运行后果为:true
那么为什么会这样呢?不是说相当于StringBuilder新建存储在堆中吗?让咱们一起来剖析剖析
如果拼接符号左右两边都是字符串常量或常量援用,则依然应用编译期优化,即非StringBuilder的形式
针对于final润饰类、办法、根本数据类型、援用数据类型的量的构造时,能应用上final的时候倡议应用上
从字节码角度来看:为变量 s4 赋值时,间接应用 #16 符号援用,即字符串常量 “ab”
接下来咱们应用一个拼接操作与append 操作的效率进行比照
public void test6(){
long start = System.currentTimeMillis();
method1(100000);//4014
long end = System.currentTimeMillis();
System.out.println("破费的工夫为:" + (end - start));
}
public void method1(int highLevel){
String src = "";
for(int i = 0;i < highLevel;i++){
src = src + "a";//每次循环都会创立一个StringBuilder、String
}
}
//运行后果如下:
破费的工夫:4014
public void test6(){
long start = System.currentTimeMillis();
method2(100000);
long end = System.currentTimeMillis();
System.out.println("破费的工夫为:" + (end - start));
}
public void method2(int highLevel){
//只须要创立一个StringBuilder
StringBuilder src = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
}
//运行后果如下:
破费的工夫:7
通过输入破费的工夫咱们能够领会执行效率:通过StringBuilder的append()的形式增加字符串的效率要远高于应用String的字符串拼接形式!
起因是因为StringBuilder的append()的形式:从头至尾中只创立过一个StringBuilder的对象
那么对于应用String的字符串拼接形式有有余呢:
- 创立过多个StringBuilder和String(调的toString办法)的对象,内存占用更大;
- 如果进行GC,须要破费额定的工夫(在拼接的过程中产生的一些两头字符串可能永远也用不到,会产生大量垃圾字符串)。
五、intern()的应用
咱们先看看intern在String类是什么怎么形容的呢?
咱们看图就能够晓得intern是一个native办法,调用的是底层C的办法
字符串常量池池最后是空的,由String类私有地保护。在调用intern办法时,如果池中曾经蕴含了由equals(object)办法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被增加到池中,并返回对该字符串对象的地址。(这是源码里的大略翻译)
如果不是用双引号申明的String对象
,能够应用String提供的intern办法:从字符串常量池中查问以后字符串是否存在
,若不存在就会将以后字符串放入常量池中。比方:
String myInfo = new string("I love you").intern();
咱们说new String()寄存在堆空间,那么就会在堆空间创立”I love you “同时去常量池判断是否有这个”I love you”,若不存在则将以后字符串放入常量池中同时返回地址给myInfo
这样的话也就说如果在任意字符串上调用String.intern办法,那么其返回后果所指向的那个类实例,必须和间接以常量模式呈现的字符串实例完全相同。因而,下列表达式的值必然是true
("a"+"b"+"c").intern()=="abc"
艰深点讲interned 对于String就是确保字符串在内存里只有一份拷贝,这样能够节约内存空间,放慢字符串操作工作的执行速度。留神:这个值不存在会创立并存放在字符串外部池(String Intern Pool)
对于 new String() 的阐明
================================
上面咱们看看这个问题:new String(“ab”)会创立几个对象?
依据咱们后面的思路,咱们还间接观看字节码吧,看看到底做了些什么事件
new #2 <java/lang/String>:
在堆中创立了一个 String 对象ldc #3 <ab> :
在字符串常量池中放入 “ab”(如果之前字符串常量池中没有 “ab” 的话)
上面咱们看看这个问题:new String(“a”) + new String(“b”) 会创立几个对象?
依据咱们后面的思路,咱们还间接观看字节码吧,看看到底做了些什么事件
new #2 <java/lang/StringBuilder> :
拼接字符串会创立一个 StringBuilder 对象new #4 <java/lang/String> :
创立 String 对象,对应于 new String(“a”)ldc #5 <a> :
在字符串常量池中放入 “a”(如果之前字符串常量池中没有 “a” 的话)new #4 <java/lang/String> :
创立 String 对象,对应于 new String(“b”)ldc #8 <b> :
在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话)invokevirtual #9 <java/lang/StringBuilder.toString> :
调用 StringBuilder 的 toString() 办法,会生成一个 String 对象
有了后面两道题做根底,咱们接下来看看一道比拟难的题目看看创立了几个对象
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == 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
目前咱们的环境是JDk7,这时咱们编译看看main办法的字节码是怎么样的
而刚刚咱们后面的第二个问题铺垫时就说到过new String() + new String()的问题解析
那么咱们就剖析一下s3.intern()是做了什么事件呢?
在后面new String(“1”) + new String(“1”)的时候会调用 StringBuilder 的 toString() 办法,会生成一个 String 对象为”11″,但咱们后面提到过在字符串常量池中并没有生成
所以当咱们执行s3.intern()的时候
字符串常量池没有s3的”11″,所以创立一个指向堆空间new String(“11”)的地址
执行s4 = “11”的时候常量池里有”11″,所以就会应用s3.intern()的那个指向地址,所以s3 == s4 为true
那么咱们一起看看这个解释的思路图吧(JDk7 环境)
那么在JDK6的思路图当中就不一样,咱们一起来看看
在JDK6当中字符串常量池并没有在堆空间,所以它会在常量池生成一个新的对象”11″并且有新的地址
所以当在JDk6中的s3 与 s4 ,它们的援用地址各不同所以s3 == s4 为false
接下来咱们依据这个个性再扩大一下,看上面的代码块输入后果是什么呢?
public class StringIntern1 {
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
String s4 = "11";
String s5 = s3.intern();
System.out.println(s3 == s4);
}
}
咱们运行起来,发现后果是运行后果:false
那么依据后面的题目剖析,咱们晓得执行new String(“1”) + new String(“1”)
字符串常量池中并不会存储”11″,当执行s4 = “11”才会在字符串常量池中存在
而s5 = s3.intern()其实是在常量池寻找是否有”11″,若有则返回指向地址给到s5
此时s3的”11″是存储在堆空间当中的,但s4的”11″是存储在字符串常量池中,所以为false
小结intern()
================================
JDK 1.6中,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,会把此
对象复制一份
,放入串池,并返回串池中的新对象地址
Jdk1.7起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,则会把
对象的援用地址复制一份放入串池
,并返回串池中的援用地址
应用intern()测试执行效率
================================
接下来咱们测试一下intern()进行执行一下效率
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]));
}
long end = System.currentTimeMillis();
System.out.println("破费的工夫为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
//运行后果如下:
破费工夫:7307
并且咱们通过Java VisualVm工具看看这段代码怎么样呢?
咱们再看看应用intern()执行同样的需要,看看它的破费工夫是多少呢?
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])).intern();
}
long end = System.currentTimeMillis();
System.out.println("破费的工夫为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
//运行后果如下:
破费工夫:1311
并且咱们通过Java VisualVm工具看看这段代码怎么样呢?
由此可见咱们能够比照两个操作
间接 new String :每个 String 对象都是 new 进去的,所以程序须要保护大量寄存在堆空间中的 String 实例,程序内存占用也会变高
应用 intern() 办法:因为数组中字符串的援用都指向字符串常量池中的字符串,所以程序须要保护的 String 对象更少,内存占用也更低
论断:
对于程序中大量应用存在的字符串时,尤其存在很多曾经反复的字符串时,应用intern()办法可能节俭很大的内存空间。
大的网站平台,须要内存中存储大量的字符串。比方社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern() 办法,就会很明显降低内存的大小
六、String Table的垃圾回收
咱们刚刚演示intern()的执行效率也证实了String 存在垃圾回收,所以试用intern()时更省
接下来咱们再通过上面的代码块来领会一下String的垃圾回收
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
应用命令:-Xma15m- -Xnx15m XX:+PrintStringTabIeStat1stIcs -XX:+PrintGCDetaiis
查看字符串常量池的信息
咱们将循环的操作先正文掉,看看未循环增加时的字符串常量池是怎么样的
这时咱们再运行起来看看,进行循环后的字符串常量池是怎么样的
若咱们将for循环的次数减少到十万的话,再运行起来是怎么样的呢?
由十万的输入后果咱们就能够晓得StringTable 区产生了垃圾回收
- 在 PSYoungGen 区产生了垃圾回收
- Number of entries 和 Number of literals 显著没有 100000
七、G1的String去重操作
对于G1中对于String有去除反复的操作,具体具体可查看官网文档:拜访入口
许多大规模的Java利用的瓶颈在于内存,测试表明在这些类型的利用外面,Java堆中存活的数据汇合差不多25%是String对象。更进一步,这外面差不多一半String对象是反复的,反复的意思是说:str1.equals(str2)= true。
堆上存在反复的String对象必然是一种内存的节约。这个我的项目将在G1垃圾收集器中实现主动继续对反复的String对象进行去重,这样就能避免浪费内存
观看官网文档能够晓得在咱们的利用中个别的堆空间里
- 堆存活数据汇合外面String对象占了25%
- 堆存活数据汇合外面反复的String对象有13.5%
- String对象的均匀长度是45
比如说一下代码块,领会一下:
Strnig str1 =new String("hello");
Strnig str2 =new String("hello");
那么对于这种状况,咱们G1是怎么操作的呢?
当垃圾收集器工作的时候会拜访堆上存活的对象。对每一个拜访的对象都会查看是否它是候选的要去重的String对象
如果是:把这个对象的一个援用插入到队列中期待后续的解决
。
这时有一个去重的线程在后盾运行解决这个队列。解决队列的时候把元素从队列删除这个元素,而后尝试去援用已有一样的String对象
应用一个Hashtable来记录所有的被String对象应用的不反复的char数组。当去重的时候会查这个Hashtable,来看堆上是否曾经存在一个截然不同的char数组
如果存在String对象会被调整援用那个数组,开释对原来的数组的援用,最终会被垃圾收集器回收掉。如果查找失败,char数组会被插入到Hashtable,这样当前的时候就能够共享这个数组了
提醒:临时理解一下,前面会详解垃圾回收器
对于去重的命令选项如下:
UseStringDeduplication(bool):
开启String去重,默认是不开启的,须要手动开启。PrintStringDeduplicationStatistics(bool):
打印具体的去重统计信息stringDeduplicationAgeThreshold(uintx):
达到这个年龄的String对象被认为是去重的候选对象
参考资料
尚硅谷:JVM虚拟机(宋红康老师)
发表回复