`
xjk2131650
  • 浏览: 55448 次
  • 性别: Icon_minigender_1
  • 来自: 河北
社区版块
存档分类
最新评论

Java 性能优化之 String 篇

    博客分类:
  • java
阅读更多

 

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()设计恰好可以共享原始文本从而达到节省内存的目的。反之,当所需要截取的字符串长度总和远小于原始文本长度时,用本文所推荐的方法将在很大程度上节省内存,在大文本数据处理中其优势显而易见。

其他 String 使用的优化建议

以上我们描述了在我们的大量文本分析案例中调用 String 的 subString方法导致内存消耗的问题,下面再列举一些其他将导致内存浪费的 String 的 API 的使用:

String 拼接的方法选择

在拼接静态字符串时,尽量用 +,因为通常编译器会对此做优化,如:

 String test = "this " + "is " + "a " + "test " + "string"


编译器会把它视为:

 String test = "this is a test string"


在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象。

String 构造的方法选择

常见的创建一个 String 可以用赋值操作符"=" 或用 new 和相应的构造函数。初学者一定会想这两种有何区别,举例如下:

 String a1 = “Hello”; 
 String a2 = new String(“Hello”); 


第一种方法创建字符串时 JVM 会查看内部的缓存池是否已有相同的字符串存在:如果有,则不再使用构造函数构造一个新的字符串,直接返回已有的字符串实例;若不存在,则分配新的内存给新创建的字符串。

第二种方法直接调用构造函数来创建字符串,如果所创建的字符串在字符串缓存池中不存在则调用构造函数创建全新的字符串,如果所创建的字符串在字符串缓存池中已有则再拷贝一份到 Java 堆中。

尽管这是一个简单明显的例子,然而在实际项目中编程者却不那么容易洞察因为这两种方式的选择而带来的性能问题。

使用构造函数 string() 带来的内存性能隐患和缓解

仍然以之前的从 csv 文件中截取 String 为例,先前我们通过用 new String() 去除返回的 String 中附带的原始 String 的方法优化了subString导致的内存消耗问题。然而,当我们下意识地使用 newString去构造一个全新的字符串而不是用赋值符来创建(重用)一个字符串时,就导致了另一个潜在的性能问题,即:重复创建大量相同的字符串。说到这里,您也许会想到使用缓存池的技术来解决这一问题,大概有如下两种方法:

方法一,使用 String 的 intern()方法返回 JVM 对字符串缓存池里相应已存在的字符串引用,从而解决内存性能问题,但这个方法并不推荐!原因在于:首先,intern() 所使用的池会是 JVM 中一个全局的池,很多情况下我们的程序并不需要如此大作用域的缓存;其次,intern() 所使用的是 JVM heap 中 PermGen 相应的区域,在 JVM 中 PermGen 是用来存放装载类和创建类实例时用到的元数据。程序运行时所使用的内存绝大部分存放在 JVM heap 的其他区域,过多得使用 intern()将导致 PermGen 过度增长而最后返回OutOfMemoryError,因为垃圾收集器不会对被缓存的 String 做垃圾回收。所以我们建议使用第二种方式。

方法二,用户自己构建缓存,这种方式的优点是更加灵活。创建 HashMap,将需缓存的 String 作为 key 和 value 存放入 HashMap。假设我们准备创建的字符串为 key,将 Map cacheMap 作为缓冲池,那么返回 key 的代码如下:

 private String getCacheWord(String key) { 
     String tmp = cacheMap.get(key); 
     if(tmp != null) { 
            return tmp; 
     } else { 
             cacheMap.put(key, key); 
             return key; 
     } 
 } 


结束语

本文通过一个实际项目中遇到的因使用 String 而导致的性能问题讲述了 String 在 JVM 中的存储结构,String 的 API 使用可能造成的性能问题以及解决方法。相信这些建议能对处理大文本分析的朋友有所帮助,同时希望文中提到的某些优化方法能被举一反三的应用在其他有关 String 的性能优化的场合。

 

分享到:
评论

相关推荐

    麦肯锡-年月―中国xx集团战略咨询项目建议书.ppt

    麦肯锡-年月―中国xx集团战略咨询项目建议书.ppt

    廖倩5.14运营款.xlsx

    廖倩5.14运营款.xlsx

    setuptools-25.0.2-py2.py3-none-any.whl

    文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    全网第一个宝宝生活记录网站root ca

    全网第一个宝宝生活记录网站,帮我写一个网站介绍,网址 https://43.136.21.175/ ,二维码分享和登录,统计记录最近时间,功能很全很实用,手机和电脑都可以访问,手机浏览器可以生成快捷方式到桌面和App一样,点右上角可以打开菜单,里面我做了图表统计可以看每天的喂养次数哪些,我们一直在用,很方便的

    yes I can.mp3

    yes I can.mp3

    IMG20240426155740.jpg

    IMG20240426155740.jpg

    4-8.py

    4-8

    基于python的舞蹈视频推荐系统相关实验设计与实现

    【作品名称】:基于python的舞蹈视频推荐系统相关实验设计与实现 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【项目介绍】:主要分为三个实验阶段,数据构建以及处理;召回阶段;排序阶段 数据构建 ####开发环境为:Windows10 + python3.8 本教程主要是针对B站舞蹈视频进行数据获取,数据来源于B站舞蹈区的中国舞和舞蹈教程两个板块,获取的视频日期为2021.08.01-2.21.12.31。主要分为五部分数据获取,每部分具体获取内容如下: 舞蹈信息表,这部分按月份进行获取,由于舞蹈教程较少,因此月份合并获取。最后将所有获取的数据整合成一张表。 舞蹈_用户交互表,这部分根据舞蹈信息表获取的视频url,获取评论区用户ID以及相关链接,并将label值设为1;为提高获取速度,将源表进行拆分,分为多张表在不同的代码以及IP环境中运行。 用户信息表,首先根据交互表的user_id获取user_id的集合,然后根据user_id获取用户的公开信息。 舞蹈封面,根据舞蹈信息表的pic的url获取封

    matlab机器人课程和书籍中的问题.zip

    matlab机器人课程和书籍中的问题.zip

    基于yolov8+pyqt5实现精美界面支持图片视频和摄像检测源码.zip

    基于yolov8+pyqt5实现精美界面支持图片视频和摄像检测源码.zip

    2-9.py

    2-9

    zigbee 简单控制LED灯状态

    zigbee 简单控制LED灯状态

    pytest-7.1.2-py3-none-any.whl

    文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    独立按键控制数码管显示0-F.zip

    独立按键控制数码管显示0-F.zip

    Scrapy-2.7.0.tar.gz

    文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    pytest-3.5.1.tar.gz

    文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    工具与方法工作手册.ppt

    工具与方法工作手册.ppt

    麦肯锡—xx数码公司发展战略咨询报告.ppt

    麦肯锡—xx数码公司发展战略咨询报告.ppt

    xx集团组织变革思路.ppt

    xx集团组织变革思路.ppt

Global site tag (gtag.js) - Google Analytics