JVM14_String的不可变性、内存分配、字符串拼接与append()、inter()、一道面试题??

String

基本特性
在这里插入图片描述
首先,String类是一个final类,所以是不可继承的,因为String类对字符串的刻画已经很完备了
在这里插入图片描述
再一点,实现了Serializable接口,也就是String方式传输数据是可以实现跨进程的,因为已经是实现了序列化的机制
实现了Comparable接口,所以String天生就可以进行排序
从JDK1.9开始,String的底层实现不再是char[] 数组,而是变成了byte[]数组
在这里插入图片描述

String存储结构的变更

JDK8及之前,String实现是char[]数组,也就是每一位都是占两个字节,这对于拉丁字符来说,是浪费了一般的存储空间的,本身拉丁字符一个字节就可以存储
从JDK9开始,String使用byte[]数组加一个编码标记来实现。
也就是说, 当是拉丁文字的时候(256位就能表示的),就使用一个字节
而如果编码是UTF_8等,那么就还是使用两个字节

在这里插入图片描述

String的不可变性

在这里插入图片描述
情况一:
在这里插入图片描述
对于字面量的定义方式,是存在字符串常量池中,而字符串常量池是不允许相同字符串出现两个的,
所以上边例子,栈帧的局部变量表中的引用变量s1和s2, 是指向堆空间的字符串常量池中的同一个"abc"字符串的

如果此时加上 s1 = “hello”;
那么s1就指向了字符串常量池中新创建的字符串"hello"

情况二:
在这里插入图片描述
s2 后边拼接了"def"
把握字符串的不可变性, 因为底层就是由数组实现的,数组就是一旦创建就不可修改长度的
开始s1 和 s2 都指向 “abc”
但是s2 拼接 “def” , 其实就是要新创建一个字符串 “abcdef”
而s1 是不会变的,还是指向 “abc”

情况三:
在这里插入图片描述
s1指向"abc"
这里试图去修改这个字符串,但其实只是调用String的replace()方法,把"abc"拷贝修改后返回给了s2,
s1并没有发生修改

一道面试题??

基本的内存图我手画了一下(有点乱,错误之处请大神指正,println()方法的调用并没有创建栈帧,还请大神指导或者本人空闲更新)
一个宗旨:java在方法传递参数时,是将变量复制一份,然后传入方法体去执行
其实主要还是change方法传入的是复制的一份内存地址,对于ch数组,指向的就是同一个堆内存的内存空间,所以修改是同步的。
对于str,在change方法中,因为String的底层是数组实现,所以创建后是不能发生改变的,其次,字面量的字符串对象创建方式,对象是在字符串常量池中出现的,在change方法中str的引用地址已经发生了改变,也没有返回引用,那么这种改变时不会影响方法外的这个str的。
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

String 内存分配

在这里插入图片描述
在这里插入图片描述
StringTable为什么会调整?
之所以要把StringTable从永久代调整到了堆空间,两点考虑:
1、永久代的大小PermSize默认是比较小的,如果String太多会导致永久代OOM。
2、永久代的垃圾回收频率是很低的,这样在产生大量不用的String时不能及时GC

在这里插入图片描述
intern方法会在字符串常量池生成字符串

案例一
对于在字符串常量池中已经存在的字符串,不会再创建同样的,而是直接使用
在这里插入图片描述
在这里插入图片描述
上面代码梳理:首先是两个方法,main和foo,main 中调用了foo方法, 这里就是两个栈帧,int i = 1;直接是放在栈帧中的局部变量表,
引用obj在局部变量表,new出的Object()在堆中创建,接着new Memory()对象也在堆中,mem指向Memory()对象
调用fo方法,参数传入是obj,
那么在foo方法中,第一个参数this,第二个参数param,param指向对象Object,
接着param调用toString()方法,会在字符串常量池创建字符串。str引用将指向这个字符串

主要是注意toString()方法,它会把字符串直接放到字符串常量池中保存,并且通过str变量指向常量池中的字符串

字符串拼接操作

在这里插入图片描述
实例1:常量与常量的拼接结果在常量池中,
在这里插入图片描述
实例二:
首先明确,如果拼接符号前后出现了变量,则相当于在堆中new String(), 结果就是拼接后的字符串。
那么这个字符串肯定是在堆中,而且堆中的对象即使相同字符串值也是不同的存储位置
如果对字符串使用了intern()方法, 那么会对字符串进行一个常量池校验,如果发现常量池有这个字符串,那么直接将地址返回,
如果没有这个字符串,那么就在常量池加载一份字符串,并把地址返回


实例三:
s1 + s2 ,看其class文件时会发现,就是使用的StringBuolder(),调用了append()方法将两个字符串进行了拼接,然后toString()变成一个字符串
在这里插入图片描述
实例四:
对于开发中写的那些不需要子类的类,那直接就建议加上final,更加安全。
final一修饰,那么这个变量就是常量了,也就是说,这个变量在编译期就可以确定值,也就作为常量来使用了。
这个时候对于s1 + s2 这样的操作,看似都是变量,其实两边就都是常量了,既然是常量那就使用常量池
在这里插入图片描述

拼接字符串和使用StringBuilder的append方法的效率问题

如果使用字符串拼接的方式来得到一个长字符串,根据前边的理解,每次变量src 和 “a” 的拼接,底层都会创建一次StringBuilder对象,然后调用append方法,拼接之后又在toString时又创建一个String对象,也就是拼接一次就会产生两个对象
而使用先new一个StringBuilder对象的方式,只会创建这一个对象,其他时间都是在调用append()方法
极大地提升了效率

具体效率高的细节:
1、字符串的拼接创建了太多StringBuilder 和String对象
2、也正因为创建了太多的对象,导致在GC时需要花费更多的时间,也导致GC出现的时间更早

在这里插入图片描述

基于上述的只new 一次的StringBuilder()操作的改进:

如果基本确定要前前后后添加的字符串长度不高于某个限定值,那么建议使用带参的构造器,直接确定字符数组的容量。
StringBuilder s = new StringBuilder(int capacity); // new char[capacity]
这样就避免了在字符串变长,而数组扩容必然会导致之前小数组被丢弃成为垃圾。
减少了数组的不断扩容

intern()的使用

intern()就是在确保内存中(字符串常量池)只有一份这个字符串
在这里插入图片描述
不管执行了什么方法,只要在最后intern(),那么就能保证指向的是字符串常量池中的字符串
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值