首页 > 其他分享 >JVM(十)StringTable

JVM(十)StringTable

时间:2023-07-12 10:47:12浏览次数:37  
标签:String StringTable System println JVM 字符串 out 常量

JVM(十)StringTable


1 String的基本特性

  • String即字符串,通过一对引号""表示,String创建的方式主要有

    • String s = "abc"; // 字面量的方式
    • String s = new String("abc"); // 类创建new方式
  • String声明为final,不可以被继承

  • String实现了Serializable接口,表示字符串是支持序列化、可以跨进程传输的;实现了Comparable接口,表示字符串可以比较大小

  • String在JDK8及以前在内部定义了final char[] value用于存储字符串数据,jdk9的时候改成了byte数组

  • String是不可变的字符序列,即具有不可变性,主要表现在:

    • 当对字符串重新赋值的时候,需要重写指定内存区域赋值,而不能使用原有的value进行赋值
    • 当对字符串进行连接操作的时候,也需要重写指定内存区域赋值,而不能使用原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或者字符串的时候,也需要重写指定内存区域赋值,而不能使用原有的value进行赋值
  • 通过字面量的方式给一个字符串赋值,此时字符串值声明在字符串常量池中

    jdk6之前字符串常量池和静态变量位于方法区中,6及之后位于堆中

  • 字符串常量池中是不会存放相同内容的字符串的(原理下面的String底层HashTable的说明)

为什么jdk9修改String的底层结构为字节数组?

​ 一些统计表名字符串是堆中使用的主要部分,而且大部分字符串对象只包含拉丁字符,这些字符只需要一个字节的存储空间,但是char型数据类型占两个字节,这也就意味着字符串对象的内部char数组中有一半的空间将不会使用,造成空间浪费,因此改变String的结构为字节数组存储能够节约空间。并且基于String的数据结构的StringBuilder和StringBuffer都做出了改变。

String底层HashTable的说明
  • String的字符串常量池是一个固定大小的HashTable,默认大小为1009
  • 如果放入String Table的字符串特别多,就会造成Hash冲突,进而使用拉链法解决冲突会导致链表越来越长,当调用String.intern时性能会大幅度下降
  • 使用-XX:StringTableSize可以设置StringTable的大小
  • jdk6中字符串常量池大小是固定1009,jdk7开始默认大小变为60013,并且1009是可以设置的最小值

2 String的内存分配

  • 在Java中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行时速度更快、更节省内存,都提供了一种常量池的概念

  • 常量池类似于一个Java系统级别的缓存,8种基本数据类型的缓存是系统协调的,String类型的常量池比较特殊,使用的方法有两种

    • 直接使用字面量赋值,声明出来的String对象会直接存储在常量池中

    • 使用String提供的intern()方法,可以将String对象放入常量池

      	String.valueOf(1).intern();
      

    Java语言规范要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且指向同一个String实例

         System.out.println("1"); // 2162
         System.out.println("2"); // 2164
         System.out.println("3"); // 2165
         System.out.println("4"); // 2166
         System.out.println("5"); // 2167
    
         // 下面的字符串存在于字符串常量池中,因此不会被再次加载
         System.out.println("1"); // 2168
         System.out.println("2"); // 2168
         System.out.println("3");
         System.out.println("4");
         System.out.println("5"); // 2168
    
  • 在jdk6及之前,字符串常量池存放在永久代

  • jdk7开始,将字符串常量池静态变量放入堆中

    StringTable为什么要调整?

    1. 永久代大小permSize默认比较小,大量字符串容易OOM
    2. 永久代的垃圾回收频率太低,容易造成空间浪费

3 String的分配:new String()到底创建了几个对象?

  • new String("a")创建了几个对象?

    两个,一个是在字符串常量池中根据字面量“a”创建的对象,另一个是在堆中创建的对象

        @Test
        public void test7() {
            String s = new String("a");
        }
    

    上面的代码的字节码文件可以证明这一点,ldc表示从字符串常量池中得到该字符串对象

    image-20230614162018915

  • new String("a") + new String("b")呢?

    七个,包括:

    • 在字符串常量池中字面量"a"对应的对象
    • 堆中new String("a")创建的对象
    • 在字符串常量池中字面量"b"对应的对象
    • 堆中new String("b")创建的对象
    • 拼接操作创建的StringBuilder("ab")对象
    • 字符串常量池中拼接而成的字面量“ab”对应的变量
    • StringBuilder("ab").toString()产生的String对象

    字节码文件通用可以证明:

        @Test
        public void test8() {
            String s = new String("a") + new String("b");
        }
    

    image-20230614162633942

    最后一个方法调用应看toString方法的字节码:

    image-20230614162930059

4 字符串拼接操作

  • 常量与常量引用之间的拼接结果在常量池,原理是编译期优化

        public static void main(String[] args) {
            var s1 = "abc";
            var s2 = "a" + "b" + "c";
            
            System.out.println(s1 == s2); // true
            System.out.println(s1.equals(s2)); // true
        }
    

    上面代码编译后变为下面结果,说明对于常量与常量的拼接进行了编译器优化,并将拼接结果放在了字符串常量池中,因此两个变量的地址相同

    	public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "abc";
            System.out.println(s1 == s2);
            System.out.println(s1.equals(s2));
        }
    

    常量引用的拼接结果也是在常量池中,仍然使用编译期优化:

        @Test
        public void test4() {
            final String s1 = "a";
            final String s2 = "b";
            String s3 = "ab";
            String s4 = s1 + s2;
            System.out.println(s3 == s4); // true
        }
    
    	@Test
        public void test4() {
            final String s1 = "a";
            String s3 = "ab";
            String s4 = s1 + "b";
            System.out.println(s3 == s4); // true
        }
    

    final修饰的变量变为了常量,修饰的类不能被继承,修饰方法不能被重写

    并且被final修饰的变量的显示赋值在编译期就完成了,而不是像其他变量在前端编译后的字节码文件还是符号引用,需要在运行的时候转化为运行时常量池的直接引用(即使用的时候直接访问而不需要在运行时确定),因此能够使用final的地方尽量都使用final

  • 常量池中不会存在相同内容的常量

  • 只要其中有一个是变量结果就在堆中,变量的拼接原理是StringBuilder,JDK5.0之前使用的是StringBuffer

  • 如果拼接的结果调用intern()方法,则主动将变量池中还没有出现过的字符串对象加载一份到字符串常量池中,并返回常量池中此对象的地址

        public void test2() {
            var s1 = "javaEE";
            var s2 = "hadoop";
            var s3 = "javaEEhadoop";
            var s4 = "javaEE" + "hadoop";
            var s5 = "javaEE" + s2;
            var s6 = s1 + "hadoop";
            var s7 = s1 + s2;
    
            System.out.println(s3 == s4); // true:常量拼接编译器优化,并且放入常量池
            System.out.println(s3 == s5); // false:拼接出现变量,结果相当于在堆空间中new String
            System.out.println(s3 == s6); // false
            System.out.println(s3 == s7); // false
            System.out.println(s5 == s6); // false
            System.out.println(s5 == s7); // false
            System.out.println(s6 == s7); // false
    
            var s8 = s6.intern();
            System.out.println(s3 == s8); // true:intern检查字符串常量池中是否存在内容相同的字符串,没有则将字符串值加载一份到字符串常量池,并返回字符串常量池中该字符串的地址
        }
    
4.1 字符串拼接的底层原理
    public void test3() {
        var s1 = "a";
        var s2 = "b";
        var s3 = "ab";
        var s4 = s1 + s2;
        System.out.println(s3 == s4); // false
    }

​ 编译后的字节码文件如下,结合对象的实例创建过程,包含变量的字符串拼接的底层实际上进行了:

  • ① StringBuilder s = new StringBuilder(),包括

    • 首先调用new操作符指令创建运行时常量池中指定索引下标的类的对象并对其进行零值初始化,即:1.将该类进行加载 2.并在堆中开辟创建对象的空间并进行零值初始化
    • (不用说这条)dup指令在栈帧的操作数栈中将指向创建对象的引用复制一份,这样就有两个引用指向堆空间的对象实体,栈底部的引用负责对对象进行赋值操作;栈顶的引用则是作为一个句柄调用相关方法;也可以看到操作数栈的深度为2
    • 执行<init>方法进行对象的显示初始化,<init>方法包括无参或者有参构造器以及代码块中的显示赋值
  • ② 执行append操作拼接字符串

  • ③ 转换为String字符串,s.toString()

    toString()方法底层与new String()类似

image-20230614140946890

练习:

    @Test
    public void test5() {
        String s1 = "javaEEhadoop";
        String s2 = "javaEE";
        String s3 = s2 + "hadoop";
        System.out.println(s1 == s3); // false

        final String s4 = "javaEE";
        String s5 = s4 + "hadoop";
        System.out.println(s1 == s5); // true
    }
4.2 字符串拼接与append操作的效率对比
  • StringBuilder拼接的方式自始至终只创建一个StringBuilder对象,而字符串拼接的过程每次拼接都会会创建一个StringBuilder和String对象(执行toString()产生的),因此会耗时更多
  • 由于字符串拼接产生了大量对象,因此内存不足的情况下会进行GC,所以耗时更多
    @Test
    public void test6() {
        long l1 = System.currentTimeMillis();
        //method1(100000); // 4031
        method2(100000); // 5
        long l2 = System.currentTimeMillis();
        System.out.println(l2 - l1);
    }

    public void method1(int times) {
        String s = "";
        while(times-- > 0) {
            s += "a";
        }
    }

    public void method2(int times) {
        StringBuilder s = new StringBuilder();
        while(times-- > 0) {
            s.append("a");
        }
    }

在实际开发中,使用StringBuilder的时候如果底层的char数组容量不足的时候会进行扩容,因此可以使用构造方法设定容量大小防止不断的扩容操作,减少耗时

即如果能够确定拼接字符串的长度,建议进行一次性扩容

 private int newCapacity(int minCapacity) {
     // overflow-conscious code
     int newCapacity = (value.length << 1) + 2;
     if (newCapacity - minCapacity < 0) {
         newCapacity = minCapacity;
     }
     return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
         ? hugeCapacity(minCapacity)
         : newCapacity;
 }

5 intern()的使用

  • String对象调用intern方法的时候,首先会判断字符串常量池中是否含有equals相同的对象,如果存在直接返回字符串常量池中该对象的地址,否则加载该字符串对象到常量池并返回其地址

  • 在任何字符串上调用String.intern方法的时候,其返回结果指向的类实例必定和以常量形式存在的字符串变量完全相同,也即是:intern方法就是确保字符串在内存里只有一份拷贝,即存放在字符串内部池中,以节省内存空间,加快字符串操作任务的执行速度

  • 确保引用指向字符串常量池的方式:

    • 以字面量的方式定义字符串
    • 字符串调用intern方法

6 SringTable的垃圾回收

​ 对虚拟机添加启动参数执行下面的代码,出现YGC

-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
public class StringGCTest {
    public static void main(String[] args) {
        for(int i = 0; i < 100000; i++) {
            String.valueOf(i).intern();
        }
    }
}

[GC (Allocation Failure) [PSYoungGen: 4096K->504K(4608K)] 4096K->664K(15872K), 0.0025364 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 4608K, used 3718K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 78% used [0x00000000ffb00000,0x00000000ffe23b10,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 11264K, used 160K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 1% used [0x00000000ff000000,0x00000000ff028000,0x00000000ffb00000)
 Metaspace       used 3308K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
  ...
G1垃圾回收器的String去重操作
  • 这里的String去重操作,指的是对堆中创建的重复字符串对象的value的去重操作,而不是对字符串常量池中进行去重,字符串常量池中的字符串始终是唯一的

    String s1 = new String("abc");
    String s2 = new String("abc");
    

    首先在方法栈帧的局部变量表中有两个引用变量s1和s2,分别指向在堆中创建的两个String对象,同时在字符串常量池中也存在一个“abc”字符串,而这里的去重,就是指的在堆中创建的两个String对象

  • Java堆中存活的数据集合25%是String对象,而差不多有一半是重复的,因此造成了内存的浪费

  • G1垃圾回收器的String去重操作的实现

    • 垃圾收集器在工作的时候,首先会访问堆上存活的对象,检查其是否是候选的需要去重的String对象
    • 使用一个HashTable来记录所有被String对象使用的不重复的数组,检查去重的时候查找这个HashTable即可
    • 如果是则将对象的引用插入到队列,等待后续的处理。这个队列由一个后台线程进行处理,处理即是从队列删除这个元素,并且去重它所指向的对象
    • 否则char数组就会被插入到HashTable
  • 命令行选项

    • UseStringDeduplication(bool)开启String去重,默认是不开启的
    • PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息
    • StringDeduplicationAgeThreshold(unitx):设置年龄阈值,到达这个年龄的对象就会被认为是去重的对象

标签:String,StringTable,System,println,JVM,字符串,out,常量
From: https://www.cnblogs.com/tod4/p/17546902.html

相关文章

  • JVM(九)执行引擎
    JVM(九)执行引擎1执行引擎概述执行引擎是Java虚拟机核心的组成部分之一虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力,区别在于物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎是由软件自主实现的,因此可以不受物理条件......
  • JVM(十三)分代收集、增量收集以及分区算法
    JVM(十三)分代收集、增量收集以及分区算法1分代收集算法​ 前面的所有算法中,没有一种算法能够完全替代其他算法,它们都有自己独特的优势和特点,分代收集算法应运而生:分代收集算法对不同生命周期的对象采取不同的收集方式,一般划分为新生代和老年代,以便提高回收效率在Java程序......
  • JVM(十二)垃圾清除阶段算法
    JVM(十二)垃圾清除阶段算法垃圾清除阶段是指,当成功区分出内存区域中的存活对象和死亡对象之后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep)......
  • JVM(十一)垃圾回收概述和垃圾标记阶段的算法
    JVM(十一)垃圾回收概述和垃圾标记阶段的算法1Java垃圾回收概述什么是垃圾?垃圾是在程序运行过程中不被任何指针指向的对象,这个对象就是需要被回收的垃圾为什么要进行垃圾回收?如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占内存空间会一直保存到应用程序结束,被......
  • 初识虚拟机JVM
    初识JVM(JAVAVirtualMachine)​ JVM是一种规范,可以使用软件来实现,也可以使用硬件来实现,就是一个虚拟的用于执行bytecodes字节码的计算机。他也定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。​ JVM负责将java字节码解释运行,边解释边运行,这样,速度就会受到一定的影......
  • JVM(六)堆
    JVM(六)堆1核心概述几乎所有的对象实例和数组都是分配在堆上的(栈不会存储数组和对象,栈帧中的局部变量表只会存储指向堆中实例的引用)一个Java进程对应一个JVM实例,一个JVM实例只存在一个堆内存,堆也是内存管理的核心区域堆和方法区是线程共享的,但堆也有划分的线程私有缓冲区......
  • JVM常用工具分析
    JVM基础分析、故障解决工具常用jdk工具jps:JvmProcessStatusTool显示系统内全部的虚拟机进程;jstat:JvmStatisticsMonitoringTool动态收集指定进程运行时数据;jinfo:ConfigurationInfoForJava实时显示或调整虚拟机的配置信息;jmap:MemoryMapForJava生......
  • jvm学习-垃圾回收的一些知识点
    部分图片和描述来自参考资料,非原创对象回收处理过程如何标定对象是否存活两种方法:引用计数方法可达性分析算法引用计数方法就和ReentrantLock可重入锁一样,内部维系着一个state,当同个线程重入结束后就会归零,但是这种方法有点问题publicstaticvoidte......
  • JVM 面试题
    JVM面试题JVM是JavaVirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与......
  • Java虚拟机(JVM):第四幕:自动内存管理 - 经典垃圾收集器
    前言:如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的实践者。整哥Java堆:FullGC。1、Serial收集器:最基础、历史最悠久的收集器,这是一个单线程工作的收集器。2、ParNew收集器:是Serial收集器的多线程并行版本,可以说是跟Serial完全一样。CMS收集......