使用jhsdb从内存地址层面理解字符串常量池

使用jhsdb从内存地址层面理解字符串常量池

  • jhsdb的简单使用
    • 打开方式
    • Heap Parameters
    • scanoops命令
    • Inspector
    • Stack Memory
  • 理解字符串常量池
    • 第一组
    • 第二组
    • 第三组

需要用到的Java代码

package nuc.ss;

public class StringOops {
    String s1;
    String s2;
    String s3;

    String str1;
    String str2;
    String str3;

    String string1;
    String string2;
    String string3;

    public static void main(String[] args) {
        StringOops stringOops = new StringOops();
        //第一组
        stringOops.s1 = new String("计算机");
        stringOops.s2 = stringOops.s1.intern();
        stringOops.s3 = "计算机";
        System.out.println(stringOops.s1 == stringOops.s2);//false
        System.out.println(stringOops.s3 == stringOops.s2);//true
        //第二组
        stringOops.str1 = new String("str") + "ing";
        stringOops.str2 = stringOops.str1.intern();
        stringOops.str3 = "string";
        System.out.println(stringOops.str1 == stringOops.str2);//true
        System.out.println(stringOops.str2 == stringOops.str3);//true
        //第三组
        stringOops.string1 = new String(new char[]{'a','b','c'});
        stringOops.string2 = stringOops.string1.intern();
        System.out.println(stringOops.string1 == stringOops.string2);//true

        System.out.println("done");//此处打断点
    }
}

jhsdb

jhsdb是一个图形化故障处理工具

打开方式

在控制台或者idea的terminal,找到需要调试进程的pid,输入以下命令

jhsdb hsdb --pid (pid)


 

Heap Parameters

打开Tools -> Heap Parameters,这个小工具可以查看虚拟机中的堆参数

可以看到新生代老年代的地址分区情况

 

scanoops

scanoops (起始内存地址) (结束内存地址) (需要查找的全限定类名)

打开Window -> Console,使用scanoops命令查找普通对象指针

 

Inspector

打开Tools -> Inspector,通过这个检查器,检查一个内存地址存放的对象

 

Stack Memory

打开Java Tread,选择main线程,查看栈帧中标识符的地址

 

理解字符串常量池

通过之前得到的信息,我们查找到标识符的位置在虚拟机栈的main栈帧中的ff728内存区域中,StringOops对象在Java堆中的36c40中。

 

第一组

//第一组
stringOops.s1 = new String("计算机");
stringOops.s2 = stringOops.s1.intern();
stringOops.s3 = "计算机";
System.out.println(stringOops.s1 == stringOops.s2);//false
System.out.println(stringOops.s3 == stringOops.s2);//true

内存情况

首先我们来查看第一组字符串对象的内存分布情况。
我们发现s2和s3指向的地址相同,证明s2和s3指向的是同一个字符串对象,而s1指向的是另一个字符串对象



 

打开s1和s2的内部详情,发现这两个字符串对象的value值指向的内容相同,也就是指向了同一个字符数组对象。


 

继续展开s1的value的详细内容,发现value指向的是一个byte数组,因为一个汉字占2个字节,所以这6个字节代表的就是"计算机"


 

字符串对象创建过程图示化

第一行语句的对象创建详细过程

stringOops.s1 = new String("计算机");
//类似于
String string = "计算机";
stringOops.s1 = new String(string);
  1. new String()会先开辟一块内存空间,但还没有初始化,value没有指向
  2. 之后执行括号中的"计算机",相当于字面量创建字符串对象,这时会调用intern()方法,检查字符串常量池中是否有"计算机"这个对象;发现没有之后,需要自己创建一个对象...
  3. 创建完毕后将这个对象的引用交给字符串常量池进行维护(图中将字符串常量池维护的字符串对象改为蓝色,字符数组对象改为黄色,绿色为不被维护的普通对象),这时括号中的"计算机"这个对象的创建过程已经执行完毕,intern方法也执行完毕。
  4. 接下来蓝色对象将作为original返回给new String()的构造方法,在这个构造方法中,蓝色对象的value值会赋给绿色对象的value
  5. 最后,绿色对象的地址会赋给s1标识符

 

所以new String("计算机");这种方式会创建2个字符串对象,以及一个字符数组对象。

第二行的过程

stringOops.s2 = stringOops.s1.intern();
  1. 直接调用s1的inter()方法,拿着绿色对象的value指向的"计算机"去查字符串常量池。
  2. 发现存在,直接将其维护的蓝色对象的引用返回给s2标识符。

 

第三行的过程

stringOops.s3 = "计算机";

直接拿"计算机"查表,查到之后直接返回引用

 

总结

最终我们可以看到,s1指向的对象和s2、s3指向的不同,但这两个字符串对象的value的指向是相同的。


第二组

//第二组
stringOops.str1 = new String("str") + "ing";
stringOops.str2 = stringOops.str1.intern();
stringOops.str3 = "string";
System.out.println(stringOops.str1 == stringOops.str2);//true
System.out.println(stringOops.str2 == stringOops.str3);//true

内存情况

str1、str2和str3指向同一个字符串对象

 

字符串对象创建过程图示化

第一行过程

stringOops.str1 = new String("str") + "ing";
  1. 第一行语句会先创建出new String("str") 和 "ing" 的字符串对象,之后会调用StringBuilder的append()方法将 "str" 和 "ing" 拼接为 "string" ,这里简单画出new String("str") 和 "ing"的创建结果,不标注地址。
  2. "str" 和 "ing" 创建好后,会创建一个StringBuilder对象,来调用append()方法来拼接字符串,之后使用toString()方法生成一个字符串对象,并返回给str1标识符。这时的字符串对象并没有被字符串常量池维护,也就是字符串常量池中没有"string"的记录。

    这里把new String("str")指向的字符串对象标红了,这时因为除了append()方法的栈帧临时引用了它一下,它就没有被别的对象引用了,下次GC就会把它回收。

 

 

 

 

第二行过程

stringOops.str2 = stringOops.str1.intern();
  1. 调用str1的intern()方法,即拿着str1指向的"string"去查字符串常量池。
  2. 在字符串常量池中没有查到 "string",那么就将str1的指向的地址直接返回给字符串常量池,字符串常量池再将该引用返回给str2。这时字符串常量池才有了 "string" 的记录。

 

第三行过程

stringOops.str3 = "string";

和第一组一样,直接拿 "string" 查表,发现存在记录,直接将字符串常量池中的引用返回。

 

总结

stringOops.str1 = new String("str") + "ing";创建字符串需要使用StringBuilder,这个过程不会将创建好的字符串 "string" 放入字符串常量池。


第三组

//第三组
stringOops.string1 = new String(new char[]{'a','b','c'});
stringOops.string2 = stringOops.string1.intern();
System.out.println(stringOops.string1 == stringOops.string2);//true

内存情况

string1和string2指向同一个字符串对象

 

字符串对象创建过程图示化

第一行过程

stringOops.string1 = new String(new char[]{'a','b','c'});

new char[];直接创建了一个字符数组对象,没有调用intern()方法,直接将字符数组对象返回给字符串构造方法。这种方式和 new String("字面量") 相比,前者只创造了一个字符串对象,而后者创建了两个;前者没有将 "abc" 放入字符串常量池,而后者将"字面量"放入了。

 

第二行过程

stringOops.string2 = stringOops.string1.intern();


和之前的一样,查表后发现没有 "abc" ,将string1的引用返回给字符串常量池维护,字符串常量池在将这个地址返回给string2。

 

总结

new String(new char[]{'a','b','c'});这种方式不调用intern方法, "abc"字符串采用懒加载方式进入字符串常量池。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值