好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

前言

String应该是Java使用最多的类吧,很少有Java程序没有使用到String的。在Java中创建对象是一件挺耗费性能的事,而且我们又经常使用相同的String对象,那么创建这些相同的对象不是白白浪费性能吗。所以就有了StringTable这一特殊的存在,StringTable叫做字符串常量池,用于存放字符串常量,这样当我们使用相同的字符串对象时,就可以直接从StringTable中获取而不用重新创建对象。那么,StringTable都有哪些特性呢?接下来就让我们好好探讨一下StringTable。

String的一些特性

String的不可变性

在讲介绍StringTable之前,就不得不提一下String的不可变性,因为只有当String是不可变的才使得StringTable的实现成为可能。当我们定义一个字符串时:

String s = "hello";

这时候,“hello”就被存放在StringTable中,而变量s是一个引用,s指向了StringTable中的“hello”。

当我们把s的值改一下,改成”hello world“

String s = "hello";
s = "hello world";

这时候,并不是原先s指向的”hello“的值改变为了”hello world“,而是指向了一个新的字符串。

如何去验证是指向了一个新的字符串而不是修改其内容呢,我们可以打印一下hash值看看。

        String s = "hello";
System.out.println(System.identityHashCode(s));
s = "hello world";
System.out.println(s.hashCode());
s = "hello";
System.out.println(System.identityHashCode(s));

可以看到,第一次和第三次的hash值一样,第二次hash值和其它两次不同,说明确实是指向了一个新的对象而不是修改了String的值。

那么String是怎么实现不可变的呢?我们来看一下String类的源码:

从源码中我们可以看出,首先String类是final的,说明其不可被继承,就不会被子类改变其不可变的特性;其次,String的底层其实是一个被final修饰的数组,说明这个value在确定值后就不能指向一个新的数组。这里我们要明确一点,被final修饰的数组虽然不能指向一个新的数组,但却是可以修改数组的值的:

既然可以被修改,那String怎么是不可变的呢?因为String类并没有提供任何一个方法去修改数组的值,所以String的不可变性是由于其底层的实现,而不是一个final。

那么String为什么要设计成不可变的呢?我觉得是因为出于安全性的考量,试想一下,在一个程序中,有多个地方同时引用了一个相同的String对象,但是你可能只是想在一个地方修改String的内容,要是String是可变的,导致了所有的String的内容都改变了,万一这是在一个重要场景下,比如传输密码什么的,不就出大问题了吗。所以String就被设计成了不可变的。

字符串的拼接

说完了String的不可变性,再来聊一聊字符串的拼接问题,看下面一段程序

public static void main(String[] args) {
String a = "hello";
String b = " world!";
String c = a+b;
}

就是这么简单了一段程序,你知道它是怎么实现的吗?我们来看一下这段代码对应的字节码指令:

我就不一行行解释这些字节码指令是什么意思了,我们重点看一下用红色标注的几行代码,看不懂前面的字节码指令没关系,可以看后面的注释。可以看到,字符串拼接其实就是调用StringBuilder的append()方法,然后调用了toString()方法返回一个新的字符串。

StringTable讲解

字符串什么时候被放入StringTable的

先来简单介绍一下StringTable。它的底层数据结构是HashTable,每个元素都是key-value结构,采用了数组+单向链表的实现方式。

再来看下面一段代码:

public static void main(String[] args) {
-> String a = "hello";
String b = " world!";
String c = "hello world!";
}

在类加载后,“hello”这些字符串仅仅是当作符号被加载进了运行时常量池中,还没有成为字符串对象,这是因为Java中的字符串采用了延迟加载的机制,就是程序运行到具体某一行的时候再去加载。比如当程序运行到箭头所指向的那一行时,“hello”会从一个符号变成一个字符串对象,然后去StringTable中找有没有相同的字符串对象,如果有的话就返回对应的地址给变量a,如果没有的话就把“hello”放入StringTable中,然后再把地址给变量a。我们来看一下是不是这样:

        String s1 = "hello world";
String s2 = "hello world";
String s3 = "hello world";
String s4 = "hello world";
System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s2));
System.out.println(System.identityHashCode(s3));
System.out.println(System.identityHashCode(s4));

可以看到,四个字符串对象的hash值都一样,说明如果StringTable中已经有了相同的对象就会指向同一个对象而不是指向新的对象。

new String()的时候都干了什么

当我们使用new String()去创建一个字符串对象时和直接写String a = "hello"是不一样的。前者保存在堆内存中,后者保存在StringTable中。

其实StringTable也是在堆中,我后面会详细说明。我们先来验证一下上面的说法:

        String a = "hello";
String b = new String("hello");
System.out.println(a == b);

看一下运行结果:

结果很显然肯定是false,说明两者确实不是一个对象。而且上面提到指向字符串常量时会先从StringTable中查找,找到就直接返回找到的字符串,但是new String()的时候却不是这样,每new 一个String就会在堆里面创建一个新的String对象,即使是相同的内容,比如我创建4个String对象。

String s1 = new String("hello world");
String s2 = new String("hello world");
String s3 = new String("hello world");
String s4 = new String("hello world");

这时候在堆里面就会存在4个String对象:

我们再来打印一下hash看看是不是4个对象:

System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s2));
System.out.println(System.identityHashCode(s3));
System.out.println(System.identityHashCode(s4));

从结果中看出,确实是4个不同的对象。

intern方法是干吗的

我们先来看一段代码:

String s1 = new String("hello world");
String s2 = "hello world";
String s3 = s1.intern(); System.out.println(s1 == s2);
System.out.println(s2 == s3);

大家看看能不能分析出结果是什么,如果你已经知道结果,说明你已经掌握了intern方法,如果不知道,就看我下面的讲解。

结果是false和true,intern方法是干吗的呢?

intern方法的作用就是尝试将一个字符串放入StringTable中,如果不存在就放入StringTable并返回StringTable中的地址,如果存在的话就直接返回StringTable中的地址。这是jdk1.8版本中intern方法的作用,jdk1.6版本中有些不同,1.6中intern尝试将字符串对象放入StringTable,如果有则并不会放入,如果没有会把此对象复制一份,放入StringTable, 再把StringTable中的对象返回。不过我们在这里不讨论1.6版本。

解释一下上面的代码:首先我们在堆中创建了一个"hello world"字符串对象,s1指向了这个堆中的对象;然后在StringTable中创建了一个值为"hello world"的字符串常量对象,s2指向了这个StringTable中的对象;最后我们尝试将s1指向的堆中对象放入StringTable中,发现已经有了,所以就返回了StringTable中的字符串对象的地址给了s3。所以s1和s2指向了同一个对象,s2和s3是一个对象。就像下图这样:

要是把代码稍微改一下呢:

String s1 = new String("hello world").intern();
String s2 = "hello world"; System.out.println(s1 == s2);

这时候结果就是true了。我们来分析一下:首先使用了new String()在堆中创建了字符串对象,然后调用了其intern()方法,所以就从StringTable中查找有没有同样的字符串,发现没有,就将字符串放入StringTable中,然后将StringTable中的对象的地址给了s1;到第二行的时候,因为没有用new String(),所以就直接从StringTable中查找,发现有,就将StringTable中的对象的地址给了s2;所以s1、s2指向了同一个对象。

StringTable的位置

前面已经提到了StringTable在堆中,现在来验证一下。验证的方式很简单,我们放入大量的字符串导致内存溢出,看看是哪个部分内存溢出就知道StringTable在哪儿了。

ArrayList list = new ArrayList();
String str = "hello";
for(int i = 0;i < Integer.MAX_VALUE;i++) {
String s = str + i;
str = s;
list.add(s.intern());
}

我们先是调用了intern方法将字符串放入StringTable,再用一个ArrayList去存放字符串,目的是为了避免垃圾回收,因为这样的话每个字符串都会被强引用,就不会被垃圾回收了,垃圾回收了就不会看到我们想要的结果。来看一下结果:

很明显,是堆内存发生了内存溢出,这样就可以确定StringTable是存放在堆中的。不过这是从1.7版本开始的,1.7之前保存在永久代中。

StringTable的垃圾回收

既然前面提到了垃圾回收,我们就来验证一下StringTable会不会发生垃圾回收。还是上面的代码,只不过稍微修改一下:

String str = "hello";
for(int i = 0;i < 10000;i++) {
String s = str + i;
s.intern();
}

这里没有再将字符串放入ArrayList了,要不然就算是发生了内存溢出也不会垃圾回收。为了看到垃圾回收的过程,所以添加几个虚拟机参数,先不指定堆大小:

运行程序,看看打印情况:

因为堆内存足够大,所以没有发生垃圾回收,我们现在将堆内存设置的小一点,,来个1m:

-Xmx1m

再来运行下程序:

这回因为堆内存不够,发生了多次垃圾回收,所以说,StringTable也会因为内存不足导致垃圾回收

StringTable底层实现以及性能调优

在介绍性能调优之前不得不说一说StringTable的底层实现,前面已经提到了StringTable底层是一个HashTable,HashTable长什么样呢?其实就是数组+链表,每个元素是一个key-value。当存入一个元素的时候,就会将其key通过hash函数计算得出数组的下标并存放在对应的位置。

比如现在有一个key-value,这个key通过hash函数计算结果为2,那么就把value存放在数组下标为2的位置。但是如果现在又有一个key通过hash函数计算出了相同的结果,比如也是2,但2的位置已经有值了,这种现象就叫做哈希冲突,怎么解决呢?这里采用了链表法:

链表法就是将下标一样的元素通过链表的形式串起来,如果数组容量很小但是元素很多,那么发生哈希冲突的概率就会提高。大家都知道,链表的效率远没有数组那么高,哈希冲突过多会影响性能。所以为了减少哈希冲突的概率,所以可以适当的增加数组的大小。数组的每一格在StringTable中叫做bucket,我们可以增加bucket的数量来提高性能,默认的数量为60013个,来看一个对比:

long startTime = System.nanoTime();
String str = "hello";
for(int i = 0;i < 500000;i++) {
String s = str + i;
s.intern();
}
long endTime = System.nanoTime();
System.out.println("花费的时间为:"+(endTime-startTime)/1000000 + "毫秒");

先通过一个虚拟机参数将bucket指定的小一点,来个2000吧:

 -XX:StringTableSize=2000

运行一下:

一共花费了1.2秒。再来将bucket的数量增加一点,来个20000个:

 -XX:StringTableSize=20000

运行一下:

可以看到,这次只花了0.19秒,性能有了明显的提升,说明这样确实可以优化StringTable。这里只介绍了一种提升性能的方法,篇幅有限,就不再多说了,我以后可能会专门写一篇文章来专门讲讲StringTable性能优化的问题。

写在最后

文章到这里就结束了,可能内容上有些纰漏,大家将就着看吧,毕竟水平有限。如果你觉得我的文章写的对你有些帮助,请不要忘了点赞,转发,收藏,关注哦!

看了这篇文章,我搞懂了StringTable的更多相关文章

  1. flutter系列之:flutter架构什么的,看完这篇文章就全懂了

    目录 简介 Flutter的架构图 embedder engine Flutter framework Widgets Widgets的可扩展性 Widgets的状态管理 渲染和布局 总结 简介 Fl ...

  2. [转帖]看完这篇文章你还敢说你懂JVM吗?

    看完这篇文章你还敢说你懂JVM吗? 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约 600m,Linux自身使用 ...

  3. [转帖]看完这篇文章,我奶奶都懂了https的原理

    看完这篇文章,我奶奶都懂了https的原理 http://www.17coding.info/article/22 非对称算法 以及 CA证书 公钥 核心是 大的质数不一分解 还有 就是 椭圆曲线算法 ...

  4. 基础篇|一文搞懂RNN(循环神经网络)

    基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...

  5. 一篇文章,读懂Netty的高性能架构之道

    一篇文章,读懂Netty的高性能架构之道 Netty是由JBOSS提供的一个java开源框架,是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架, ...

  6. APP的缓存文件到底应该存在哪?看完这篇文章你应该就自己清楚了

    APP的缓存文件到底应该存在哪?看完这篇文章你应该就自己清楚了 彻底理解android中的内部存储与外部存储 存储在内部还是外部 所有的Android设备均有两个文件存储区域:"intern ...

  7. 【转帖】我以为我对Kafka很了解,直到我看了这篇文章

    我以为我对Kafka很了解,直到我看了这篇文章 2019-08-12 18:05 https://www.sohu.com/a/333235171_463994?spm=smpc.author.fd- ...

  8. 细心看完这篇文章,刷新对Javascript Prototype的理解

    var person={name:'ninja'}; person.prototype.sayName=function(){ return this.name; } 分析上面这段代码,看看有没有问题 ...

  9. 一篇文章,读懂 Netty 的高性能架构之道

    原文 Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机 ...

  10. Python正则表达式,看完这篇文章就够了...#华为云&#183;寻找黑马程序员#【华为云技术分享】

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/devcloud/article/detai ...

随机推荐

  1. Python——assert、isinstance的用法

    1.assert 函数说明: assert语句是一种插入调试断点到程序的一种便捷的方式. 使用范例 assert 3 == 3 assert 1 == True assert (4 == 4) pri ...

  2. 龙芯开源社区上线.NET主页

    龙芯团队从2019年7 月份开始着手.NET Core的MIPS64支持研发,经过将近一年的研发,在2020年6月18日完成了里程碑性的工作,在github CoreCLR 仓库:https://gi ...

  3. Python List comprehension列表推导式

    http://blog.chinaunix.net/uid-28631822-id-3488324.html 具体内容需要进一步学习

  4. JavaScript学习 Ⅶ

    十四. DOM(文档对象模型) 节点:Node--构成HTML文档最基本的单元 文档节点:整个HTML文档 元素节点:HTMl文档中的HTML标签 属性节点:元素的属性 文本节点:HTML标签中的文本 ...

  5. C与lua交互--lua调用栈分析(2)

    0 预备知识: 至少对Lua手册C API有简单的了解.lua5.3手册中文 1  Lua虚拟机的栈,如图: 假设虚拟机的栈有n个数据: 解释: 两种解释方式: A,栈顶开始, -1 ...-n B, ...

  6. spring security简单登录的认证

    一.思路 1.先导入相关配置(使用spring security校验之后,登录拦截的配置) 2.创建一个 WebSecurityConfig 继承 WebSecurityConfigurerAdapt ...

  7. mysql groupby 字段合并问题(group_concat)

    在我们的日常mysql查询中,我们可能会遇到这样的情况: 对表中的所有记录进行分类,并且我需要得到每个分类中某个字段的全部成员. 上面的话,大家看起来可能不太好懂,下面举一个例子来给大家说明. 现在我 ...

  8. 日志套餐篇 - log4j2 logback全量套餐

    日志套餐篇 - log4j2 logback全量套餐 前情提要: Log4j Log4j2 logback是当下主流的日志框架 slf4j则是新一代的日志框架接口,logback直接实现了slf4j接 ...

  9. 题解 UVA1193 Radar Installation

    原题 PDF OJ 思路 分析 因为半径d已经确定,所以对于每个点,我们可以算出它在x 轴上的覆盖位置线段LR,如图. 此问题便转为: 对于 n 个区间,每个区间内至少有1个点,求最少点数. 算法 我 ...

  10. ES6语法——Promise对象

    一.概念 Promise是异步编程的一种解决方案(解决回调地狱的问题),是一个能够获取异步操作信息的对象.Promise的内部保存着某个未来才会结束的事件(通常是一个异步操作) 二.特点 1.Prom ...