JVM上篇_13_StringTable_尚硅谷

1 String的基本特性

String的基本特性

  • String: 字符串,使用一对“”引起来标识。
  • String:声明为final的,不可被继承
  • String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:标识String可以比较大小
  • String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9改为了byte[]

String在jdk9中存储结构变更

官方文档:https://openjdk.java.net/jeps/254

Motivation

The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

Description

We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

结论:String再也不用char[]来存储了,改成byte[]加上编码标记,节省了一些空间。

public final class String 
    implements java.io.Serializable,Comparable<String>, CharSequence {
    
    
    @Stable
    private final byte[] value;
}

那StringBuffer和StringBuilder是否仍无动于衷呢?

String-related classes suah as AbstractStringBuilder,StringBuilder,and StringBuffer will be updated to use the same representation,as will the HotSpot VM‘s intrinsic(固有的、内置的)string operations.

也做了相应的修改

字符串常量池

  • String: 代表不可变的字符序列。简称:不可变性。
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
  • 字符串常量池中是不会存储相同内容的字符串的。
  • String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致连表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
  • 使用-XX:StringTableSize可设置StringTable的长度
  • 在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求
  • 在jdk7中,StringTable的长度默认值是60013
  • jdk8开始,设置StringTable的长度的话,1009是可设置的最小值。
jinfo -flag StringTableSize 12512
-XX:StringTableSize=60013

2 String的内存分配

String的内存分配

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

  • 常量池就类似于一个Java系统级别提供的缓存。8中基本数据类型的常量池都是系统协调的。String的常量池比较特殊。它的主要使用方法有两种

    • 直接使用双引号声明出来的String对象会直接存储在长啊凌迟中。

      比如:String info = “hello”;

    • 如果不是用双引号声明的String对象,可以使用Stringt提供的intern()方法。这个后面介绍

  • Java 6及以前,字符串常量池存放在永久代

  • Java7中Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。

    • 所有的字符串都保存在堆(Heap)中,和普通的对象一样,这样可以让你进行调优应用时,仅需要调整堆大小就可以了。
    • 字符串常量池概念原先使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java7中使用String,inter()。
  • Java8元空间,字符串常量在堆

image-20220616221150670

StringTable为什么要调整? ①permSize默认比较小 ②永久代垃圾回收频率低

image-20220616222711080

使用String导致堆OOM例子

/**
 * jdk6中
 * -XX:PermSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 *
 * jdk8中
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 */
public class StringTest3 {
    
    
  public static void main(String[] args) {
    
    
    // 使用Set保持着常量池引用,避免full gc回收常量池行为
    Set<String> set = new HashSet<String>();
    // 在short可以取值的范围内足以让6MB的Permiseze或heap产生OOM了。
    short i = 0;
    while (true) {
    
    
      set.add(String.valueOf(i++).intern());
    }
  }
}

3 String的基本操作

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

查看String字面量个数的变化

public class StringTest4 {
    
    
  public static void main(String[] args) {
    
    
    System.out.println();    // 2252 代表内存中字符串的数量,详细操作看下图
    System.out.println("1"); // 2253
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10");// 2362

    System.out.println("1"); // 2363
    System.out.println("2"); // 2363
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10"); // 2363
  }
}

以debug模式运行,并在第一行打断点,查看debug控制台里类的数量

image-20220616223420652

image-20220616223432177

image-20220616223444863

4 字符串拼接操作

  1. 常量与常量的拼接结果在常量池,原理是编译器优化
  2. 常量池中不会存在相同内容的常量。
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结构调用intern()方法,则主动将常量池中还没有的字符创对象放入池中,并返回地址。

5 intern()的使用

inern()的使用

image-20220616224028151

​ 如果不是双引号声明的String的对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

  • 比如:String myInfo = new String(" lys ");

也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true:

(“a" + “b” + “c”).intern() == ”abc"

通俗点将,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)

Intern()的使用:jdk6 vs jdk7/8

public static void main(String[] args) {
    
    
  String s = new String("1");
  s.intern();
  String s2 = "1";
  System.out.println(s == s2); // false

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

题目:new String(“ab”)会创建几个对象?

new String("ab") 会创建几个对象?看字节码,就知道是两个
(1)new关键字在堆空间创建的
(2)字符串常量池有个“ab”对象。字节码指令:ldc

题目:new String("a) + new String(“b”) 创建了几个对象

对象1:new StringBuilder()
对象2:new String("a")
对象3:字符常量池”a"
对象4:new String("b")
对象5:字符串常量池“b"

image-20220616231234915

image-20220616231259198

image-20220616231306719

总结String的intern()的使用:

  • jdk6中,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • jdk7起,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份放入串池,并返回串池中的引用地址。

image-20220617001206415

image-20220617001215280

intern()的效率测试:空间角度

/**
 * 使用intern()测试执行效率:空间使用上
 * 结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间
 */
public class StringInterSpaceTest {
    
    
  static final int MAX_COUNT = 1000 * 10000;
  static final String[] arr = new String[MAX_COUNT];

  public static void main(String[] args) throws InterruptedException {
    
    
    Integer[] data = new Integer[]{
    
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    long start = System.currentTimeMillis();
    for (int i = 0; i < MAX_COUNT; i++) {
    
    
      arr[i] = new String(String.valueOf(data[i % data.length]));
//      arr[i] = new String(String.valueOf(data[i % data.length])).intern();
    }

    long end = System.currentTimeMillis();
    System.out.println("花费的时间为:" + (end - start));

    Thread.sleep(10000000);
    System.gc();
  }
}

​ 大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern()方法,就会明显降低内存的大小。(主要实现是需要使用的变量都使用intern()去返回,堆空间的值就可以被垃圾回收,而使用的对象用的是字符串常量池里的数据)

6 StringTable的垃圾回收

7 G1中的String去重操作

官方文档:https://openjdk.org/jeps/192

JEP 192: String Deduplication in G1

Summary

Reduce the Java heap live-data set by enhancing the G1 garbage collector so that duplicate instances of String are automatically and continuously deduplicated.

Motivation

Many large-scale Java applications are currently bottlenecked on memory. Measurements have shown that roughly 25% of the Java heap live data set in these types of applications is consumed by String objects. Further, roughly half of those String objects are duplicates, where duplicates means string1.equals(string2) is true. Having duplicate String objects on the heap is, essentially, just a waste of memory. This project will implement automatic and continuous String deduplication in the G1 garbage collector to avoid wasting memory and reduce the memory footprint.

G1的String去重操作

  • 背景:对许多Java应用(有大的也有小的)做的测试得出一下结果:

    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  • 许多大规模的Java应用的瓶颈在于内存,测试表名,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:

    string1.equals(string2)==true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。

  • 实现

    • 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否候选的要去重的String对象。
    • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着队列删除这个元素,然后尝试去重他引用的String对象。
    • 使用一个hashtable来记录所有的别String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
    • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
    • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
  • 命令行选项

    • UseStringDeduplication(bool):开启String去重,默认是不开启的,需要手动开启。
    • PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息
    • StringDeduplicationAgeThreshp;d(uintx):达到这个年龄的String对象被认为是去重的候选对象。

猜你喜欢

转载自blog.csdn.net/weixin_43811294/article/details/125462300
今日推荐