前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >字符串常量池,看这篇就够了(三)

字符串常量池,看这篇就够了(三)

原创
作者头像
子牙老师
发布2022-04-20 09:45:23
6480
发布2022-04-20 09:45:23
举报
文章被收录于专栏:手写JVM专栏手写JVM专栏

哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。

手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO、三色标记算法…

这篇文章是专栏字符串常量池的第三篇。如果前两篇你还没看,墙裂都建议你回去看一下,再来看本篇

本篇文章就从上篇文章留的问题切入,分享:

  1. 什么情况字符串会写入常量池
  2. 什么情况字符串不会写入常量池
  3. intern底层是如何实现的
  4. 字符串过多导致OOM如何解决

上篇留的问题是这段代码为什么是这个结果

简单分析一下

一、s2与s3不是同一个对象,说明在创建s3这个对象时,字符串常量池StringTable中是没有[子牙真帅]这个字符串的。引出的问题就是什么情况字符串会写入?什么情况不会?

二、执行s3.intern,如果接收返回结果,则s3与s4指向的是同一个对象,如果不接收,还是不相等,这又是为什么呢?

写不写常量池

一般我们用Java代码创建字符串,常用方式有三

代码语言:javascript
复制
String s1 = "ziya";
String s2 = new String("ziya");
String s3 = "zi" + new String("ya");

那不常用的呢?反射方式创建、字节码增强包创建。但是我想,正常写代码,没人这么不正常吧。

如果你的Java代码编译生成的字节码指令中有【ldc】,就会写入常量池,否则就不会。细节上篇文章已经讲了,不赘述。

经过前面的简单分析我们知道:字符串拼接是不会写入常量池的,我们来细究一下

首先我们来看下字符串拼接,经编译器编译后生成的字节码长啥样子

是不是没想到,字符串拼接的底层竟然是通过StringBuild类实现的,通过append进行拼接,调用toString转成字符串。那毫无疑问,字符串拼接不写入常量池的秘密,不是在append中,就是在toString中。

追踪append的调用链,你最终会找到这句代码

代码语言:javascript
复制
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) {
    ……
    System.arraycopy(this.value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    ……
}

StringBuild中维护着一个能够自动扩容的char数组,append传入的字符串,都会被蜕去外壳,拿到真正的字符串内容,写入这个char数组。

arraycopy是个native方法,继续跟踪到Hotspot源码,发现也没有操作StringTable的代码。Hotspot源码我就不贴了,敢兴趣的可以自己去看。不会看吗?可以看我之前写的一篇文章《教你如何找到native方法对应的Hotspot源码》

再看看toString调用链,代码很简单,创建一个String对象,将StingBuild中char数组中的内容原封不动的copy过来

代码语言:javascript
复制
public String(char[] value, int offset, int count) {
    ……
    this.value = Arrays.copyOfRange(value, offset, offset + count);
    ……
}

copyOfRange最终调用的也是arraycopy这个native方法

综上:你写的Java代码经编译器编译后生成的字节码指令中有【ldc】,就会写入常量池,字符串拼接不会写入常量池。

intern做了什么

JVM执行到指令【ldc】就会将字符串写入常量池,本质原因是这条调用链上会调用intern,那intern底层是如何实现的呢?我把相关代码贴出来,然后挨个剖析

先上第一段代码

解释这个代码之前,先给大家看个图,不然讲了大家也听不懂。什么图呢?字符串到底是怎么存储的?与运行时常量池之间的关系是怎样的?

前面讲过,在link阶段,字符串会被封装成Symbol对象,存储到SymbolTable中,然后还要存储到运行时常量池中

运行时常量池是一个C++对象,如图所示:蓝色部分表示这个C++对象本身占用的内存,接在对象后面的部分是常量池项。比如index=1的位置是一个字符串,那这个位置存储的就是一个Symbol对象的内存地址

运行时常量池这个C++对象中有一个属性resolved_references,是一个数组结构。看这个属性的名字也能看得出来:解析过的引用类型。即如果这个字符串已经执行过了intern,由Symbol对象转成了String对象,就直接返回。如果还没有转成String对象,就调用intern,调用完intern还有件重要的事情,就是写入resolved_references。

这三段内容看到,上面那段代码应该不用我解释了吧。

接下来第二段代码

这段代码做的事情:从Symbol对象中拿出字符串内容,调用intern。这个方法代码有点长,保留核心逻辑

1、先去字符串常量池StringTable中去找有没有这个字符串,如果有,直接返回,如果没有,往下走

2、第16行代码,基于字符串内容创建Java的String对象。这个方法等下展开讲,讲完第二篇的内容你就恍然大悟了

3、将创建的String对象写入StringTable。这个做的好处:一、下次通用的字符串不需要再次执行创建,提升了程序执行效率;二、由于不需要重复创建,节省了内存,有点缓存的感觉

接下来看下16行代码的细节

189行:创建一个Java的String对象,这里是Hotspot源码,所以创建的是一个oop对象,转成Handle。Hotspot源码中,操作一个对象,有时候是直接操作oop,有时候会转成Handle对象。其实因为做了C++级别的操作符重载,两种对象的写代码风格风格基本差不多

190行:拿到String对象中存储字符串的容器char数组,对应的Hotspot中的C++对象就是typeArrayOop

后面的for循环就是一个字节一个字节的赋值。这块为啥不调用类似memcopy直接整块内存拷贝呢?想不通!

到这里就把intern底层细节讲明白了

字符串导致OOM

这个问题也是小伙伴问我问的比较多的。看到这里你应该清楚这背后的原因及如何解决了吧

背后的原因是大概率你的代码中的字符串都是拼接生成的,不会写入常量池,所以每次都是不断的创建,消耗内存空间

解决办法就是在拼接字符串的代码后面手动调用intern触发写入常量池StringTable。后面出现的G1的字符串去重,本质就是干这事,就是你不用手动调用intern,在GC的时候,G1给你做了。

但是就算存在字符串去重,因为拼接底层实现是通过copy实现的,不会写入常量池,所以字符串去重只是缓解了这个问题,并没有根本解决这个问题。如果从根本上解决,拼接的底层实现需要改!jdk8这块还是之前的代码,后面的不晓得改了没,抽空看下

题外话

子牙手写JVM小班四期正在招生。四期新增了字节码增强+Agent,学完你就可以做JVM相关的工作,如二开arthas,自研类hsdb调试器、自研实现热更新热部署零侵入日志等黑科技…

四期完整课程包含七大专题+一个增值专题,约50多个课时。完整学完你就可以:1、用Java写一个Java虚拟机,从而深入理解运行系统的底层细节;2、有能力自行研究Hotspot源码及其他用C语言、C++写的中间件源码;3、能够用C语言、C++写任何你感兴趣的基础算法如:内存池、垃圾回收算法、主从同步算法、执行引擎、存储引擎;4、就有底子跟着我学习下半年准备开的操作系统内核班……

这套课程,横跨多个计算机学科,但只是一个学科的价格。这套课程,JVM专家、功力深厚、经验丰富的子牙老师亲授,跟我学习不踩坑,全网唯一教授虚拟机的课程…

感兴趣小伙伴可以加班班微信咨询(jvm-anan),真诚招生,无任何套路。课程试看,问题真诚解答,全部了解清楚再上车。一二三期共500多VIP加入,无一人退费,好评不断

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写不写常量池
  • intern做了什么
  • 字符串导致OOM
  • 题外话
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档