详解JVM 中的StringTable

是什么

字符串常量池是 JVM中的一个重要结构,用于存储JVM运行时产生的字符串。在JDK7之前在方法区中,存储的是字符串常量。而字符串常量池在 JDK7开始移入堆中,随之而来的是除了存储字符串常量外,还可以存储字符串引用(因为在堆中,引用堆中的字符串常量很方便,所以可以存储引用)。这使得很多字符串的操作在 JDK7中和在之前的版本中执行是不同的结果。这也是为什么字符串相关的问题是如此具有迷惑性的原因之一。

底层

String:在 JDK9之前,String底层是使用 char数组来存储字符串数据的,而在 JDK9开始,使用 byte数组+编码来代替 char数组,这是为了节省空间,因为不同编码的数据占空间不一样,很多单位数据只需要一个 byte(8字节) 就可以存储,而使用 char(16字节)就会浪费多余的空间。

字符串常量池:底层使用 HashTable来存储字符串,在 JDK6 HashTable的数组长度是1006,JDK7开始变成了 60013,这是为了避免存储字符串过多导致链表长度过长从而查询效率降低。可以使用参数 -XX:StringTableSize=来设置 StringTable数组的长度。

常见问题字符串相加

1、对于字符串常量相加,编译器会优化成直接相加。

如 String ss = "a" + "b",在编译器的优化下,实际上只会创建一个 "ab" 字符串。

而 final String s1 = "a"; String s2 = s1+"b",除了创建字符串 "a"外,只会创建 "ab"。

操作相关字符串如下:

可以看到只对字符串 "a"、"ab"进行了入池操作(ldc)

2、对于包含字符串变量的相加,不会在字符串常量池中创建对应的字符串。
如 String s1 = "a"; String s2 = s1 + "b",执行完后字符串常量池中只会包含 "a"、"b" 字符串。

对于 s1 + "b",下面是其字节码操作

可以看到,相加操作实际上是调用 StringBuilder的append方法进行字符串拼接,然后调用它的 toString方法获取返回值保存输出,期间并没有入池操作(ldc)。

由此得出的优化建议:因为每次执行一次包含非常量的字符串相加时,都进行了一次 StringBuilder对象的创建,所以如果需要多次连接,可以直接创建 StringBuilder对象,使用一个 StringBuilder对象进行字符串拼接,避免创建多个对象降低效率。

对象创建数量

对象,包括 new的对象以及字符串对象。

1、对于String ss = new String ("ab"),这个过程首先会在会在字符串常量池中创建一个 "ab"字符串常量,然后再在堆上创建一个 new String()的对象,在这个对象中会保存常量池中 "ab"的地址信息,最后在栈上创建一个局部变量 ss ,保存堆中创建的对象地址。所以全程创建了堆中的一个对象和字符串常量池中的一个对象。

2、new String("a") + new String("b")。严格来看,创建了六个对象。

首先new String("a")和 new String("b") ,分为创建了两个对象。两者相加时,会创建一个 StringBuilder对象,而在 StringBuilder.toString()方法中,也会创建一个 String对象

3、String s1 = "a", String s2 = "b", String s3 = "a" + "b" + s1 + "c" + s2;对应的字节码如下:

字符串常量池中会有四个字符串对象,分别是 "a"、"b"、"ab"、"c"。在开始因为 s1、s2的赋值,会将 "a"、"b"分别加入字符串常量池,然后执行第三步,运行顺序是从左到右,首先执行 "a" + "b" ,因为两个都是常量,所以会因为编译器的优化直接返回 "ab",并且因为计算的两个参数都是常量,所以直接加入字符串常量池,随后因为与变量 s1相加,所以调用 StringBuilder的append方法,得到的结果保存到局部变量表中,所以引入常量 "c",因为是常量,所以还是会引入字符串常量池,然后与前面拼接得到的结果再次拼接,最后再与变量 s2相加,因为不是常量所以还是不会将结果加入字符串常量池。

除此之外,还需要注意,上面三种情况是在初始情况下,也就是字符串常量池中没有要加入的字符串时的场景,如果字符串常量池中预先就包含要加入的字符串,那么就会直接将常量池中的对应的字符串地址返回给调用方。比如 String s1 = "a",在常量池中没有 "a"时,创建的对象是 1个,而如果常量池中已经存在,那么就会将其地址直接返回赋给 s1。那么创建的对象就是 0个了。

intern()与字符串相等判断

intern()方法是 String类的一个native方法,作用是尝试将调用这个方法的字符串对象加入字符串常量池中,然后返回常量池中存储的值。在开头说过,在 JDK7开始字符串常量池可以存储字符串引用,导致字符串操作的过程可能会之前不一样,从而得到不同的结果。

intern()方法的执行:

1.6及之前:尝试将当前字符串常量加入常量池,如果常量池存在就返回地址值;如果不存在就先加入常量池,然后再返回加入位置的地址值。

1.7开始:尝试将当前字符串常量加入常量池,如果存在就将返回地址值;如果不存在就存入当前 String 字符串的地址值。

下面以一个例子来解释一下,在JDK7和JDK7之前下面代码执行分别是什么结果。

@Test
 public void test1(){
  String s = new String("1");
  s.intern();
  String s2 = "1";
  System.out.println(s == s2);

  String s3 = new String("1") + new String("1");
  s3.intern();
  String s4 = "11";
  System.out.println(s3 == s4);
 }

先说结论:

JDK7之前: false、false。

JDK7及之后:false、true。

原因:

1、首先先看上面 3------6行的,首先,第三行会在字符串常量池中添加 "1" ,然后在堆中创建一个对象,保存 "1"在常量池中的地址,再在局部变量表中添加一个 s保存堆中对象的地址。随后执行第四行,此时 s指向的字符串已经在常量池中了,所以这一步无效,第五行因为常量池已经存在 "1" ,所以 JDK7或之前执行的逻辑是一样的,直接将 "1"在常量池中的地址返回给 s2。然后判断,s指向的是堆中的对象,而 s2指向的是常量池中的字符串常量,所以无论是 JDK7还是之前的都是 false。

2、然后再看下面 9-----12行。因为前面已经在常量池中添加 "1",所以第9行会直接返回地址,然后执行添加操作,创建字符串 "11",此时并没有添加到常量池,然后执行第10行,因为常量池不存在 "11",所以 JDK7之前直接加入常量池,JDK7及以后则直接将 "11"的地址存入常量池,而 s3则不变,还是保存的是常量池外的那个 "11"的地址值。然后执行 11行,因为常量池已存在 "11",所以 s4就是返回 "11"的地址值,不同的是在 JDK7之前因为常量池保存的是 "11"常量,所以返回的是常量池中的地址值;而 JDK7 及以后常量池保存的是常量池外的 "11"的地址值,所以返回的是池外的地址值。所以最后判断在 JDK7之前是 false,而在 JDK7开始是 true。

到此这篇关于JVM 中的StringTable的文章就介绍到这了,更多相关JVM 中的StringTable内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java虚拟机JVM之server模式与client模式的区别

    JVM client模式和Server模式区别 JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升. JVM工作在Server模式下可以大大提高性能,Server模式下应用的启动速度会比client模式慢大概10%,但运行速度比Client VM要快至少有10倍 当不指定运行模式参数时,虚拟机启动检测主机是否为服务器,如果是,则以Server模式启动,否则以client模式启动,J2SE5.0检测的根据是

  • 优化Java虚拟机总结(jvm调优)

    堆设置 -Xmx3550m:设置JVM最大堆内存为3550M. -Xms3550m:设置JVM初始堆内存为3550M.此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存. -Xss128k:设置每个线程的栈大小.JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K.应当根据应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右. -Xmn2g:设置堆

  • Java VisualVM监控远程JVM(详解)

    我们经常需要对我们的开发的软件做各种测试, 软件对系统资源的使用情况更是不可少, 目前有多个监控工具, 相比JProfiler对系统资源尤其是内存的消耗是非常庞大,JDK1.6开始自带的VisualVM就是不错的监控工具. 这个工具就在JAVA_HOME\bin\目录下的jvisualvm.exe, 双击这个文件就能看到一个比较直观的界面 从左边Applications树中可以知道,不光可以监控本地JVM运行情况, 还可以监控远程机器上的JVM运行情况. 本地监控:只要打开某个JAVA程序就会自

  • Java虚拟机JVM优化实战的过程全记录

    前言 Java虚拟机是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一.Java虚拟机是通过在实际的计算机上仿真模拟各种计算机功能模拟来实现的,通过Java虚拟机,您只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行. 最近在看JVM群里有人发了一个GC情况,让人帮忙看优化的,于是我也凑热闹发了出来想让群里的大神们指导优化一下,以下是优化过程记录. 一开始我贴了下面的两张图 jstat看GC记录

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • 详解JVM 中的StringTable

    是什么 字符串常量池是 JVM中的一个重要结构,用于存储JVM运行时产生的字符串.在JDK7之前在方法区中,存储的是字符串常量.而字符串常量池在 JDK7开始移入堆中,随之而来的是除了存储字符串常量外,还可以存储字符串引用(因为在堆中,引用堆中的字符串常量很方便,所以可以存储引用).这使得很多字符串的操作在 JDK7中和在之前的版本中执行是不同的结果.这也是为什么字符串相关的问题是如此具有迷惑性的原因之一. 底层 String:在 JDK9之前,String底层是使用 char数组来存储字符串数

  • 详解Jvm中时区设置方式

    这篇文章memo一下Jvm中关于时区设定的基础操作. Java的时区设定 这里列出如下三种方式 方式 说明 TimeZone.setDefault方式 通过java的utils下的TimeZone进行动态设定 user.timezone传递方式 运行时通过传递-Duser.timezone=xxx进行设定 TZ环境变量方式 通过export的TZ环境变量进行设定 TimeZone.setDefault方式 Sample代码如下: sh-4.2# cat TestTimeZone.java imp

  • 详解jvm中的标量替换

    概述 通常在java中创建一个对象,大家都认为是在堆中创建. 在jdk6开始有逃逸分析,标量替换等技术,关于在堆中创建对象不再绝对. 关于标量替换,通过以下几点进行概述: 逃逸分析 标量替换是什么 测试标量替换 逃逸分析 逃逸分析是一种分析技术,分析对象的动态作用域,供其他优化措施提供依据.比如分析一个对象不会逃逸到方法之外或线程之外,其它优化措施(栈上分配,标量替换等)根据逃逸程度进行优化. 逃逸分析示例 public class EscapeAnalysis { public Person

  • 详解JVM中的GC调优

    那些GC的默认值 其实GC或者说JVM的参数非常非常的多,有控制内存使用的: 有控制JIT的: 有控制分代比例的,也有控制GC并发的: 当然,大部分的参数其实并不需要我们自行去调整,JVM会很好的动态帮我们设置这些变量的值. 如果我们不去设置这些值,那么对GC性能比较有影响的参数和他们的默认值有哪些呢? GC的选择 我们知道JVM中的GC有很多种,不同的GC选择对java程序的性能影响还是比较大的. 在JDK9之后,G1已经是默认的垃圾回收器了. 我们看一下G1的调优参数. G1是基于分代技术的

  • 详解JVM中的本机内存跟踪

    1.概述 有没有想过为什么Java应用程序通过众所周知的-Xms和-Xmx调优标志消耗的内存比指定数量多得多?出于各种原因和可能的优化,JVM可以分配额外的本机内存.这些额外的分配最终会使消耗的内存超出-Xmx限制. 在本教程中,我们将列举JVM中的一些常见内存分配源,以及它们的大小调整标志,然后学习如何使用本机内存跟踪监视它们. 2.原生分配 堆通常是Java应用程序中最大的内存使用者,但还有其他人.除了堆之外,JVM还从本机内存中分配出一个相当大的块来维护类的元数据,应用程序代码,JIT生成

  • 详解Java中String类的各种用法

    目录 一.创建字符串 二.字符.字节与字符串的转换 1.字符与字符串的转换 2.字节与字符串的转换 三.字符串的比较 1.字符串常量池 2.字符串内容比较 四.字符串查找 五.字符串替换 六.字符串拆分 七.字符串截取 八.String类中其它的常用方法 九.StringBuffer 和 StringBuilder 1.StringBuilder与StringBuffer的区别 2.StringBuilder与StringBuffer常用的方法 十.对字符串引用的理解 一.创建字符串 创建字符串

  • 详解Kotlin中的变量和方法

    详解Kotlin中的变量和方法 变量 Kotlin 有两个关键字定义变量:var 和 val, 变量的类型在后面. var 定义的是可变变量,变量可以被重复赋值.val 定义的是只读变量,相当于java的final变量. 变量的类型,如果可以根据赋值推测,可以省略. var name: String = "jason" name = "jame" val max = 10 常量 Java 定义常量用关键字 static final, Kotlin 没有static,

  • 详解Java中的 枚举与泛型

    详解Java中的 枚举与泛型 一:首先从枚举开始说起 枚举类型是JDK5.0的新特征.Sun引进了一个全新的关键字enum来定义一个枚举类.下面就是一个典型枚举类型的定义: public enum Color{ RED,BLUE,BLACK,YELLOW,GREEN } 显然,enum很像特殊的class,实际上enum声明定义的类型就是一个类. 而这些类都是类库中Enum类的子类(Java.lang.Enum).它们继承了这个Enum中的许多有用的方法.我们对代码编译之后发现,编译器将 enu

  • 详解java中的四种代码块

    在java中用{}括起来的称为代码块,代码块可分为以下四种: 一.简介 1.普通代码块: 类中方法的方法体 2.构造代码块: 构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行. 3.静态代码块: 用static{}包裹起来的代码片段,只会执行一次.静态代码块优先于构造块执行. 4.同步代码块: 使用synchronized(){}包裹起来的代码块,在多线程环境下,对共享数据的读写操作是需要互斥进行的,否则会导致数据的不一致性.同步代码块需要写在方法中. 二.静态代码块和构造

  • 详解java中的static关键字

    Java中的static关键字可以用于修饰变量.方法.代码块和类,还可以与import关键字联合使用,使用的方式不同赋予了static关键字不同的作用,且在开发中使用广泛,这里做一下深入了解. 静态资源(静态变量与静态方法) 被static关键字修饰的变量和方法统一属于类的静态资源,是类实例之间共享的.被static关键字修饰的变量.方法属于类变量.类方法,可以通过[类名.变量名].[类名.方法名]直接引用,而不需要派生一个类实例出来. 静态资源分类存放的好处 JDK把不同的静态资源放在了不同的

随机推荐