好好学习,天天向上

本文已收录至我的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. 大致掌握django的功能

    目录 静态文件配置 request对象方法初识 pycharm链接数据库(mysql) django链接数据库(mysql) django orm 字段的增删查改 数据的增删查改 数据的查,改,删 d ...

  2. Tomcat 架构原理解析到架构设计借鉴

    Tomcat 发展这么多年,已经比较成熟稳定.在如今『追新求快』的时代,Tomcat 作为 Java Web 开发必备的工具似乎变成了『熟悉的陌生人』,难道说如今就没有必要深入学习它了么?学习它我们又 ...

  3. Redis批量查询模板

    场景 在开发的时候经常会遇到批量取缓存的问题,例如查询商品信息 传入一个商品Id列表,查询Redis数据存在则放入返回列表 不存在的数据查找数据库,并放入Redis 上面两步数据整合返回 伪代码为 l ...

  4. pytest框架使用教程

    Pytest框架 一.简介 pytest:基于unittest之上的单元测试框架 有什么特点? 自动发现测试模块和测试方法 断言更加方便,assert + 表达式,例如 assert 1 == 1 灵 ...

  5. JavaScript学习 Ⅰ

    一. JavaScript的使用 <script>标签 在HTML中,JavaScript代码必须位于<script>与</script>标签之间. 实例: < ...

  6. redis(十九):Redis 架构模式,特点

    单机版 特点:简单 问题: 1.内存容量有限 2.处理能力有限 3.无法高可用. 主从复制 Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的 ...

  7. Flask 基础组件(四):模板

    1.模板的使用 1.1  语法 1.1.1 流程控制 逻辑语法 Jinja2模板语言中的 for {% for foo in g %} {% endfor %} Jinja2模板语言中的 if {% ...

  8. J.U.C体系进阶(二):juc-locks 锁框架

    Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-locks 锁框架 接口说明 Lock接口 类型 名称 void loc ...

  9. web测试中不容忽视的细节

    最近在自动化测试的圈子里,我总是碰到很多人在群里和其他地方问为什么这个会出现错误? 为什么这个运行不了?为什么我百度了还是没用? 其实真正的原因可能是你忽略了下面这些需要注意的小地方: 1.页面分辨率 ...

  10. for语句例题:编写程序FooBizBaz.java,从1循环到150并在每行打印一个值

    /** * 编写程序FooBizBaz.java,从1循环到150并在每行打印一个值, * 另外在每个3的倍数行上打印出"foo",在每个5的倍数行上打印"biz&quo ...