intern与字符串常量池源码解析

背景知识

StringTable称作字符串常量池,是一个放在堆的结构,作用是减少String对象的创建。
intern()在底层调用存在两种情况,即第一次遇到的被动调用或Java代码层主动intern()调用。
当需要从常量池读取数据,且数据恰好是string时,如果常量池索引在0-255,使用ldc,如果常量池索引在256-65535,使用ldc_w
本文基于对JVM有一定了解的人群。

调用流程

第一次遇到时的intern()

一是code属性表中字节码指令(如图ldc或者ldc_w)时,操作对象(如图pc[1])的常量池索引偏移计算后恰好是CONSTANT_String_info(如图JVM_CONSTANT_String),并且这个对象没有被解析(result==NULL),那么会陷入reslove_ldc(),通过多层调用最终进入string_at_impl。
在这里插入图片描述
在这里插入图片描述
在这个方法中取出该引用的UTF数组,然后调用intern(),在其中生成字符串对象并将其存入字符串常量池(Stringtable::intern()),并把返回的字符串也放入已解析表(string_at_put())
在这里插入图片描述

主动调用intern()

二是使用invokespecial调用intern(),如图,调用了string.c中Java_java_lang_String_intern()
在这里插入图片描述
最后将string对象的指针传入了StringTable::intern()
在这里插入图片描述

两者相遇

这是以上两种进入方法的StrngTable::intern()的调用逻辑,可以看到以下第一个就是把symbol指向的UTF数组改为Unicode数组(as_unicode()),而第二个就是直接取出unicode数组(as_unicode_string()),这一层将不同入口进行了统一,然后调用了相同的方法(intern())
在这里插入图片描述

根据unicode数组可以从字符串常量池查找字符串(look_up()),如果找到直接返回(found_string!=NULL),如果没有找到,那就需要存入,如果string没有创建过,(可以看到上面两个方法,如果通过symbol传入,那么直接声明一个空的Handle String,如果通过string对象传入,那么会有Handle h_string),也就是string_or_null.is_null,创建字符串(这一步放在查找后面可以有效减少解析阶段多余的字符串创建)。然后通过basic_add存入字符串常量池,再返回字符串。
在这里插入图片描述

easy小测

双引号常量是否会存入字符串常量池

IDEA都知道你还不知道吗
在这里插入图片描述

new String()会不会存入字符串常量池

为什么这个是new String(),见问题3
结果是不会,只有先完成了s1.intern()才会(其调用优先级高于==计算)。
在这里插入图片描述

在这里插入图片描述

以下进行了几次intern操作

在这里插入图片描述

以下代码会创建几个字符串

int b = 0;
int c = 1;
String s1 = c+b+"a"+b;

答案是1次,对于c与b,使用的是iadd,对于其它类型和数组,+的底层实际上是StringBuilder.append,这个方法的作用是,将新传进来的字符串给放入自己的value字节数组,并没有产生新的字符串,也不会存入字符串常量池。调用toString()时,实际上是调用了new String()。只有"a"被解析的那一次(即图中字节码17行)。

intern使用场景

如果一个字符串会被长期使用,那就可以将其存入字符串常量池,这样可以有效节约空间
以下案例假设业务需求一个具体地理位置,我们知道,一个国家的省和市都是非常有限的,会频繁出现,所以采用intern()进行保存:

public class Location{
    String Province;
    String city;
    String detail;
    public void setProvince(String province)
    {
        this.province = province.intern();
    }
    public void setCity(String city)
    {
        this.city = city.intern();
    }
    public void setDetail(String detail)
    {
        this.detail = detail;
    }
}

如果province不使用intern而使用detail一样使用直接赋值,那么如果传进来的是一个本身就是字符串常量池里对应的字符串,则没有影响。
但如果不是,而是一个全新的字符串对象,那这里Location对象就多了一个指向新对象的指针了,但实际上这个新对象的存在是毫无意义的,因为我们只需要存在36个省字符串对象,多余的指针会导致垃圾迟迟无法回收而浪费了内存空间。

升华猜想

在上面源码中,我们发现了常量池中有两张表,即constant->resolved_references出现的解析表和constant->tag_at().value()的常量池表,每次完成intern()调用后,会使用string_at_put()把完成解析的对象放入解析表用于下一次调用。所以我们可以大胆推断一下类加载过程,加载阶段进行类的方法区构建时,会把字节码文件的常量池表,加载到JVM内存中,也生成一张常量池表。准备阶段会根据字段表的静态变量分配空间,在解析阶段其实可以直接无视,然后从初始化阶段开始,当调用的字节码指令涉及到了常量池字符引用,再进行常量池符号引用的解析,变为指向分配好的内存区域。如何寻找到这块分配好的内存区域呢,本人认为和Class对象与offset有关

声明

对于源码部分,本人大量参考了柏羲(黄俊)
源码部分以下均本人自己思考所得
来源是本人语雀笔记(语雀分享竟然收费了!失踪人口回归)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值