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

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

手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO、三色标记算法…
字符串常量池,看这篇就够了(三)_第1张图片

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

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

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

上篇留的问题是这段代码为什么是这个结果
字符串常量池,看这篇就够了(三)_第2张图片

简单分析一下

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

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


写不写常量池

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

字符串常量池,看这篇就够了(三)_第3张图片

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

如果你的Java代码编译生成的字节码指令中有【ldc】,就会写入常量池,否则就不会。细节上篇文章已经讲了,不赘述。
字符串常量池,看这篇就够了(三)_第4张图片

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

首先我们来看下字符串拼接,经编译器编译后生成的字节码长啥样子
字符串常量池,看这篇就够了(三)_第5张图片

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

追踪append的调用链,你最终会找到这句代码
字符串常量池,看这篇就够了(三)_第6张图片

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

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

再看看toString调用链,代码很简单,创建一个String对象,将StingBuild中char数组中的内容原封不动的copy过来
字符串常量池,看这篇就够了(三)_第7张图片

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

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


intern做了什么

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

先上第一段代码
字符串常量池,看这篇就够了(三)_第8张图片

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

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

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

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

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

接下来第二段代码
字符串常量池,看这篇就够了(三)_第10张图片

这段代码做的事情:从Symbol对象中拿出字符串内容,调用intern。这个方法代码有点长,保留核心逻辑
字符串常量池,看这篇就够了(三)_第11张图片

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

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

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

接下来看下16行代码的细节
字符串常量池,看这篇就够了(三)_第12张图片
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加入,无一人退费,好评不断

字符串常量池,看这篇就够了(三)_第13张图片

你可能感兴趣的:(java,jvm)