【JVM学习笔记】超重点——字符串String

本文详细探讨了Java中String的不可变性,通过实例展示了其工作原理。同时,分析了String对象在内存中的分配,包括字符串常量池的变化。此外,还讨论了字符串拼接、intern()方法的使用及其影响,以及不同Java版本下的行为差异。文章揭示了在大量重复字符串场景下使用intern()节省内存的重要性,并介绍了G1垃圾收集器的String去重操作。
摘要由CSDN通过智能技术生成

一、String的不可变性

String特点
String特性

二、String底层的HashTable结构

  1st,看一个例子:

public class StringExr {

	String str = new String("good");
	char[] ch = new char[]{'t', 'e', 's', 't'};

	public void change(String str, char[] ch) {
		str = "test ok";
		ch[0] = 'b';
	}

	public static void main(String[] args) {
		StringExr exr = new StringExr();
		exr.change(exr.str, exr.ch);
		System.out.println(exr.str);
		System.out.println(exr.ch);
	}

}

运行结果
  怎么样,是不是很诡异。此即String的不可变性。

String不可变性
StringTable

三、String的内存分配

  Table设置的大则浪费空间,但存取快。设置太小省空间存取慢。
类型信息
内存分配
  jdk7中,StringTable为什么要调整位置?① permSize比较小,容易OOM。② 永久代GC频率较低。

四、字符串的拼接

拼接

五、intern()方法

概述:

intern
  如何保证某字符串实例对象str指向的是字符串常量池中的数据?

    方法一:字面量赋值(String str = “Hello world!”;)

    方法二:调用intern()方法(str = str.intern();)
对intern的总结

问题一:

  如下代码会创建几个对象?

public class Test {

	public static void main(String[] args) {
		String str = new String("Hello world!");
	}

}

  查看main方法字节码:

 #2: <java/lang/String>
 #3: <Hello world!>
 #4: <java/lang/String.<init> : (Ljava/lang/String;)V>
 
 0 new #2			// 创建一个对象,为它在对空间中开辟一片内存,并将其引用值推向栈顶
 3 dup				// 将栈顶元素复制一份并压入栈顶
 4 ldc #3			// 将int、float或String型常量从常量从常量池中推入栈顶	
 6 invokespecial #4	// 执行方法(构造器,超类构造器或私有方法)
 9 astore_1			// 将栈顶元素保存到局部变量表
10 return			// 从当前方法返回空

  很容易能看出来创造了两个对象,常量池中一个,堆空间中一个。

问题二:

  如下代码会创建几个对象?

public class Test {

	public static void main(String[] args) {
		String str = new String("Hello") + new String("World");
	}

}

  查看main方法字节码:

#2: <java/lang/StringBuilder>
#3: <java/lang/StringBuilder.<init> : ()V>
#4: <java/lang/String>
#5: <Hello>
#6: <java/lang/String.<init> : (Ljava/lang/String;)V>
#7: <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
#8: <World>
#9<java/lang/StringBuilder.toString : ()Ljava/lang/String;>

 0 new #2			// 创建SB对象并入栈
 3 dup				// 复制栈顶
 4 invokespecial #3	// 执行SB的空参构造器
 7 new #4			// 创建String对象
10 dup
11 ldc #5			// 将常量池中字符串推入栈顶
13 invokespecial #6	// 执行String构造器
16 invokevirtual #7 // 执行SB的append方法
19 new #4			// 创建String对象
22 dup
23 ldc #8
25 invokespecial #6
28 invokevirtual #7 
31 invokevirtual #9 // 执行SB的toString方法
34 astore_1
35 return

  所以是六个对象:① SB ② new String(“Hello”) ③ “Hello” ④ new String(“World”) ⑤ “World” ⑥ SB的toString方法里面的new String

问题三:

  分析以下代码的运行结果及出现的原因。

public class Test {

	public static void main(String[] args) {

		String s1 = new String("Hello");
		s1.intern();
		String s2 = "Hello";
		System.out.println(s1 == s2);

		String s3 = new String("Hello") + new String("World");
		s3.intern();
		String s4 = "HelloWorld";
		System.out.println(s3 == s4);

	}

}

  查看字节码:

#2:  <java/lang/String>
#3:  <Hello>
#4:  <java/lang/String.<init> : (Ljava/lang/String;)V>
#5:  <java/lang/String.intern : ()Ljava/lang/String;>
#6:  <java/lang/System.out : Ljava/io/PrintStream;>
#7:  <java/io/PrintStream.println : (Z)V>
#8:  <java/lang/StringBuilder>
#9:  <java/lang/StringBuilder.<init> : ()V>
#10: <java/lang/String.<init> : (Ljava/lang/String;)V>
#11: <World>
#12: <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
#13: <HelloWorld>

 0 new #2
 3 dup
 4 ldc #3
 6 invokespecial #4
 9 astore_1
10 aload_1
11 invokevirtual #5
14 pop
15 ldc #3
17 astore_2
18 getstatic #6
21 aload_1
22 aload_2
23 if_acmpne 30
26 iconst_1
27 goto 31
30 iconst_0
31 invokevirtual #7
34 new #8
37 dup
38 invokespecial #9
41 new #2
44 dup
45 ldc #3
47 invokespecial #4
50 invokevirtual #10
53 new #2
56 dup
57 ldc #11
59 invokespecial #4
62 invokevirtual #10
65 invokevirtual #12
68 astore_3
69 aload_3
70 invokevirtual #5
73 pop
74 ldc #13
76 astore 4
78 getstatic #6
81 aload_3
82 aload 4
84 if_acmpne 91
87 iconst_1
88 goto 92
91 iconst_0
92 invokevirtual #7
95 return

  运行结果:在jdk6中,两个false,而在jdk7/8中,是一个false,一个true。

  第一个,由于字符串常量池中已存在"Hello",所以该函数不会在字符串常量池中存储s2,所以最后输出false。

  而第二个,首先对于jdk6而言,很好解释,s3是堆空间中,s4是永久代,区域不一样,指针也不可能一样。而对于jdk7/8,创建s3后,堆空间中出现了这个对象,再要通过intern往常量池中添加这个对象,只需要添加这个对象在堆空间中的地址就好了。所以s3指向堆空间,s4指向常量池,但常量池中又是对象在堆空间中的地址,最终还是指向堆空间。所以返回true。

问题四:

  分析以下代码的运行结果及出现的原因。

public class Test {

	public static void main(String[] args) {

		String s3 = new String("Hello") + new String("World");
		String s4 = "HelloWorld";
		s3.intern();
		System.out.println(s3 == s4);

	}

}

  输出false,因为s4声明十已经往字符串常量池中放入这个对象了,注意,不是堆空间实例的地址,而是实实在在的一个对象。这样一来,s3指向堆空间,而s4指向的是字符串常量池。而执行intern的时候就会发现字符串常量池中已有该对象,便除了返回指向常量池的引用(返回值未赋值给任何变量),不会进行任何操作。

问题五:
public class Test {

	public static void main(String[] args) {

		String s1 = new String("Hello") + new String("World");
		String s2 = s1.intern();

		System.out.println(s1 == "HelloWorld");
		System.out.println(s2 == "HelloWorld");

	}

}

  jdk6中false true,jdk7/8中,true true。

问题六:
public class Test {

	public static void main(String[] args) {

		String s1 = new String("Hello") + new String("World");
		s1.intern();
		String s2 = "HelloWorld";
		System.out.println(s1 == s2);

	}

}

  jdk7/8中返回true,jdk6中返回false

总结

  如果程序中出现大量的重复字符串,那么使用itern可以节省内存空间。

六、G1垃圾收集器的String去重操作

背景
实现方式
默认情况下不开启去重操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

九死九歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值