String 在 JVM 的存储结构

  • 一般而言,Java 对象在虚拟机的结构如下:

    • 对象头(object header):8 个字节
    • Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存如  1. Java 各数据类型所占内存.
    • 引用(reference):4 个字节
    • 填充符(padding

    表 1. Java 各数据类型所占内存

    数据类型

    占用内存(字节数)

    boolean

    1

    byte

     

    char

    2

    short

     

    int

    4

    float

     

    long

    8

    double

     

    然而,一个 Java 对象实际还会占用些额外的空间,如:对象的 class 信息、ID、在虚拟机中的状态。在 Oracle JDK 的 Hotspot 虚拟机中,一个普通的对象需要额外 8 个字节。

    如果对于 String(JDK 6)的成员变量声明如下:

      private final char value[];
      private final int offset;
      private final int count;
      private int hash;

    那么因该如何计算该 String 所占的空间?

    首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

    那么一个空 String 所占空间为:

    对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 char 数组的引用 (4 字节 ) = 40 字节。

    因此一个实际的 String 所占空间的计算公式如下:

    8*( ( 8+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

    其中,n 为字符串长度。

    案例分析

    在我们的大规模文本分析的案例中,程序需要统计一个 300MB 的 csv 文件所有单词的出现次数,分析发现共有 20,000 左右的唯一单词,假设每个单词平均包含 15 个字母,这样根据上述公式,一个单词平均占用 75 bytes. 那么这样 75 * 20,000 = 1500000,即约为 1.5M 左右。但实际发现有上百兆的空间被占用。 实际使用的内存之所以与预估的产生如此大的差异是因为程序大量使用 String.split() 或String.substring()来获取单词。在 JDK 1.6 中 String.substring(int, int)的源码为:

     public String substring(int beginIndex, int endIndex) {
          if (beginIndex < 0) {
               throw new StringIndexOutOfBoundsException(beginIndex);
          }
          if (endIndex > count) {
               throw new StringIndexOutOfBoundsException(endIndex);
          }
          if (beginIndex > endIndex) {
               throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
          }
          return ((beginIndex == 0) && (endIndex == count)) ? this :
               new String(offset + beginIndex, endIndex - beginIndex, value);
     }

    调用的 String 构造函数源码为:

     String(int offset, int count, char value[]) {
     this.value = value;
     this.offset = offset;
     this.count = count;
     }

    仔细观察粗体这行代码我们发现 String.substring()所返回的 String 仍然会保存原始 String, 这就是 20,000 个平均长度的单词竟然占用了上百兆的内存的原因。 一个 csv 文件中每一行都是一份很长的数据,包含了上千的单词,最后被 String.split() 或 String.substring()截取出的每一个单词仍旧包含了其原先所在的上下文中,因而导致了出乎意料的大量的内存消耗。

    当然,JDK String 的源码设计当然有着其合理之处,对于通过 String.split()或 String.substring()截取出大量 String 的操作,这种设计在很多时候可以很大程度的节省内存,因为这些 String 都复用了原始 String,只是通过 int 类型的 start, end 等值来标识每一个 String。 而对于我们的案例,从一个巨大的 String 截取少数 String 为以后所用,这样的设计则造成大量冗余数据。 因此有关通过 String.split()或String.substring()截取 String 的操作的结论如下:

    • 对于从大文本中截取少量字符串的应用,String.substring()将会导致内存的过度浪费。
    • 对于从一般文本中截取一定数量的字符串,截取的字符串长度总和与原始文本长度相差不大,现有的 String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。

    既然导致大量内存占用的根源是 String.substring()返回结果中包含大量原始 String,那么一个显而易见的减少内存浪费的的途径就是去除这些原始 String。办法有很多种,在此我们采取比较直观的一种,即再次调用 newString构造一个的仅包含截取出的字符串的 String,我们可调用 String.toCharArray()方法:

     String newString = new String(smallString.toCharArray());

    举一个极端例子,假设要从一个字符串中获取所有连续的非空子串,字符串长度为 n,如果用 JDK 本身提供的 String.substring() 方法,则总共的连续非空子串个数为:

    n+(n-1)+(n-2)+ +1 = n*(n+1)/2 =O(n2)

    由于每个子串所占的空间为常数,故空间复杂度也为 O(n2)。

    如果用本文建议的方法,即构造一个内容相同的新的字符串,则所需空间正比于子串的长度,则所需空间复杂度为:

    1*n+2*(n-1)+3*(n-2)+ +n*1 = (n3+3*n2+2*n)/6 = O(n3)

    所以,从以上定量的分析看来,当需要截取的字符串长度总和大于等于原始文本长度,本文所建议的方法带来的空间复杂度反而高了,而现有的 String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。反之,当所需要截取的字符串长度总和远小于原始文本长度时,用本文所推荐的方法将在很大程度上节省内存,在大文本数据处理中其优势显而易见。

     

    源文档 <http://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
StringJVM中的存储方式是通过一个char数组来存储字符串的字符数据。在JDK8及以前的版本中,String内部定义了一个final char\[\] value来存储字符串数据。而在JDK9中,String存储结构发生了变化,改为了使用byte\[\]来存储字符串数据。\[2\] String对象JVM中的存储位置有两种情况: 1. 字符串常量池:在JDK中,双引号括起来的字符串常量,例如"abc"、"def",都是直接存储在方法区的字符串常量池中。这是因为字符串在实际开发中使用非常频繁,为了提高执行效率,将字符串放在字符串常量池中。 2. 堆内存:使用new关键字创建的字符串对象会在堆内存中开辟空间。例如在示例代码中,使用new关键字创建的字符串对象"c",会在堆内存中开辟空间存储字符串数据。\[3\] 需要注意的是,字符串是不可变的,即一旦创建就不能修改。这是因为String类被声明为final,不可被继承,并且String对象JVM中是不可变的。这意味着一旦字符串对象被创建,它的值就不能被改变。\[3\] #### 引用[.reference_title] - *1* [【JavaString字符串在JVM中的存储及其内存地址问题](https://blog.csdn.net/weixin_43390123/article/details/124376835)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [JVM上篇_13_StringTable_尚硅谷](https://blog.csdn.net/weixin_43811294/article/details/125462300)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Java基础String字符串存储原理](https://blog.csdn.net/qq_46096136/article/details/126533585)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值