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

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

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

手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO、三色标记算法…
d3b8f24914385ee06837b75e757e4864_1647053362739-b189214a-0a48-4db8-8785-cb45beb28b7b.png

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

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

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

上篇留的问题是这段代码为什么是这个结果
7bd1d443a54ea64cfb7e2ca94e633c26_1649206045641-ec1e24b9-fb23-486e-9103-bc780e426de3_x-oss-process=image%2Fresize%2Cw_803%2Climit_0.png

简单分析一下

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

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

写不写常量池

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

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

如果你的Java代码编译生成的字节码指令中有【ldc】,就会写入常量池,否则就不会。细节上篇文章已经讲了,不赘述。
1e10a23f1e0ff99f79e38b854c909239_1649376906657-a5e2699f-4d03-4fee-bfb4-31727f6c80cb.png

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

首先我们来看下字符串拼接,经编译器编译后生成的字节码长啥样子
351d067a7538e87503ccb28e8e40bc38_1649377134339-41cbad53-3dbd-4b48-a261-9b48588d644e.png

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

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

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

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

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

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

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

intern做了什么

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

先上第一段代码
b8f1a20265c0b61ddd33134e978b86a5_1649724514136-e71dfa3d-ea25-4d34-83a0-4e5cbbaede8d.png

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

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

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

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

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

接下来第二段代码
db0fd5066acbdf17f2bf8bec0692960c_1649728646958-c2a63242-5d2f-472f-8c35-36fc5055bbc3.png

这段代码做的事情:从Symbol对象中拿出字符串内容,调用intern。这个方法代码有点长,保留核心逻辑
900c36247d53ca6af88264ddf093b582_1649728772091-41c6ad04-03e0-49ef-bffe-58dad87ac7b3.png

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

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

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

接下来看下16行代码的细节
25dcb578a55c6b2574a4c18b6f1c37a4_1649729055112-9250dda4-7663-4abf-ac01-20a7e10ff4fd.png

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这块还是之前的代码,后面的不晓得改了没,抽空看下

我是子牙老师,喜欢钻研底层,深入研究Windows、Linux内核、JVM。如果你也喜欢研究底层,欢迎关注我的公众号【硬核子牙】

相关文章
|
7月前
|
存储 Java
深入理解Java虚拟机:第一章
深入理解Java虚拟机:第一章
|
7月前
|
存储 算法
堆的知识点总结
堆的知识点总结
73 1
|
存储 Java 编译器
【面试题精讲】字节码构成-常量池
【面试题精讲】字节码构成-常量池
|
存储 算法 Java
Java虚拟机相关知识点1
Java虚拟机相关知识点
|
7月前
|
存储 程序员 C++
面试题:C++堆和栈的区别?
面试题:C++堆和栈的区别?
56 0
|
存储 Java
【面试题精讲】JVM-方法区-字符串常量池
【面试题精讲】JVM-方法区-字符串常量池
|
存储 Java
【面试题精讲】字符串常量池的作用了解吗?
【面试题精讲】字符串常量池的作用了解吗?
|
存储 缓存 算法
Java虚拟机相关知识点2
Java虚拟机相关知识点
|
存储 算法 安全
字符串常量池,看这篇就够了(一)
哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
274 1
字符串常量池,看这篇就够了(一)
|
Java
JVM中的一个小知识点:深堆和浅堆的概念
java中的堆内存算是整个内存区域中最重要的一块,几乎所有的对象都分配在堆内存。在堆内存中有两个主要的概念需要我们理解,这对分析java堆内存的故障有着重要的作用,分别是深堆和浅堆。
386 0
JVM中的一个小知识点:深堆和浅堆的概念

热门文章

最新文章