Java编程中的性能优化如何实现

 更新时间:2019年10月19日 08:33:33   作者:盛世半月  
这篇文章主要介绍了Java编程中的性能优化如何实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

   String作为我们使用最频繁的一种对象类型,其性能问题是最容易被忽略的。作为Java中重要的数据类型,是内存中占据空间比较大的一个对象。如何高效地使用字符串,可以帮助我们提升系统的整体性能。

  现在,我们就从String对象的实现、特性以及实际使用中的优化这几方面来入手,深入理解以下String的性能优化。

  在这之前,首先看一个问题。通过三种方式创建三个对象,然后依次两两匹配,得出的结果是什么?答案留到最后揭晓。

String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
System.out.println(str1 == str2);
System.out.println(str2 == str3);
System.out.println(str1 == str3);
  

  String对象是如何实现的?

  Java中对String对象做了大量的优化,以此来节约内存空间,提升String对象的性能。下图是Java6 -> Java9 String对象属性的变化:

  可以看到,String的属性有了以下的变化:

  • 在Java6及以前的版本中,String对象是对char数组进行了封装实现的对象,主要有:char数组、偏移量offset、字符数量count、哈希值hash。String对象通过offset和count属性来定位char数组,获取字符串。这样做可以高效快速地共享数组对象,能节省内存空间,但容易出现内存泄漏。
  • 从Java7到Java8版本,Java对String做了一些改变。String类中不再有offset和count两个属性了。这样做可以使String对象占用的内存减少,并且String.substring方法也不再共享char[],解决了可能出现的内存泄漏的问题。
  • 从Java9版本开始,将char[]改成了byte[],并增加了新属性coder,coder是一个编码格式的标识。

  为什么要这么改呢?

  我们知道,一个char字符占16位,2个字节。这种情况下存储单字节的字符就很容易浪费了。JDK1.9的String类为了节省内存空间,就使用了占8位,1个字节的byte数组来存储字符串。

  coder属性的作用是:在计算字符串长度或者使用indexOf()时,需要根据这个字段,判断如何计算字符串的长度。coder属性值默认有0和1两个值,0代表Latin-1(单字节编码),1代表UTF-16。如果String判断字符串只包含Latin-1,则coder值取0,反之为1。

  String对象的不可变性

  如果看过String的源码,就会发现,String类是被final关键字修饰的,且变量char数组也被final修饰。

  一个类被final修饰代表着该类不可继承,char[]被private和final修饰着,代表String对象不可被更改。这就叫做String对象的不可变性。即如果String对象一旦创建成功了,就不能再对它进行改变。

  这样做的好处在哪里?  

  第一、保证了String对象的安全性。假设String对象是可变的,那么String对象就会被恶意修改。

  第二.、保证hash属性值不会频繁变更,确保了唯一性。使得类似HashMap容器才能实现相应的key-value缓存功能。

  第三、可以实现字符串常量池。Java中,通常有2种创建字符串对象的方式,一种是通过字符串常量的方式创建,如String str = "abc";另一种是字符串常量通过new形式的创建,如String str = new String("abc")。

  当代码中使用第一种方式创建字符串对象时,JVM首先检查该对象是否在字符串常量池中,如果在就返回该对象的引用,否则新的字符串将在常量池中创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。

  第二种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,"abc"会在常量池中创建;然后调用new时,JVM命令将会调用String的构造函数,同时引用常量池的"abc"字符串,在堆内存中创建一个String对象,最后str引用String对象。

  

  String对象的优化

  1.如何构建超大字符串

  编程过程中字符串的拼接很常见。如果使用String对象相加,拼接我们想要的字符串,会不会产生多个对象呢?比如说以下代码:

String str = "ab" + "cd" + "ef";

  分析代码可知:首先会生成ab对象,再生成abcd对象,最后生成abcdef对象。理论上说,代码很低效。

  但实际上,会发现只有一个对象生成,这是为什么呢?编译时编译器会自动帮我们优化代码,使得最后只得出一个对象“abcdef”。

  再来看看,如果进行字符串常量的累计,又会出现什么结果?

String str = "abcdef";
for (int i = 0; i < 100; i++) {
   str = str + i;
 }

  上面的代码编译后,编译器同样对代码进行了优化,在进行字符串拼接时,偏向使用StringBuilder,这样可以提升效率。上面的代码变成了下面这样:

String str = "abcdef";
for (int i = 0; i < 100; i++) {
  str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

  总结:即使使用+号作为字符串的拼接,一样可以被编译器优化成StringBuilder的方式。但如果每次循环都生成一个新的StringBuilder实例,同样会降低系统的性能。所以平时做字符串拼接的时候,建议还是显示使用StringBuilder来提升性能。在多线程编程时,String对象的拼接涉及到了线程安全,可以使用StringBuffer。但由于StringBuffer是线程安全的,涉及到锁竞争,所以就性能上来说会比StringBuilder差些。

  2.如何使用String.intern节省内存?

  对于一些数据,数据量非常大,但同时又有大部分重合的,该如何处理呢?

  具体做法是,每次赋值的时候使用String的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象的引用,这样一开始的对象就可以被回收掉了,这样的话数据量就会大幅度降低了。

  我们再来看一个例子:

String a = new String("abc").intern();
String b = new String("abc").intern(); 
if (a == b) {
   System.out.println("a == b");
}

  输出结果是: a == b

  在字符串常量池中,默认会将对象放入常量池;在字符串变量中,对象总是创建在堆内存的,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。

  如果调用intern方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就会在常量池中新增该对象,并返回该对象引用;如果有则返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

  3.如何使用字符串的分割方法?

  spilt()方法使用了正则表达式实现了强大的分割功能,而正则表达式的性能是非常不稳定的,使用不当会引起回溯问题,很可能导致CPU居高不下。

  所以要慎重使用spilt方法,我们可以用String.indexOf()方法代替spilt()方法完成字符串的分割,如果实在无法满足需求,就在使用spilt方法时,对回溯问题加以重视就可以了。

  总结

  通过上面的叙述,我们认识到了做好String字符串性能的优化,可以提升整个系统的性能。在这个理论基础上,Java版本在迭代中不断更改成员变量,节约内存空间,对String性能进行了优化。

  我们还提到了String对象的不可变性,正是这个特性实现了字符串常量池,通过减少同一个值的字符串对象的重复创建,进一步节约内存。也是因为这个特性,我们在做长字符串的拼接时,需要显示使用StringBuilder,以提升字符串的拼接性能。最后在优化方面,我们还可以使用intern方法,让变量字符串对象重复使用常量池中相同值的对象,进而节约内存。

  最后,公布上面那道题的结果:

  false、false、true。

  其中, String str1 = “abc”;通过字面量的方式创建,abc存储于字符串常量池中;

  String str2 = new String("abc");通过new对象的方式创建字符串对象,引用地址存放在堆内存中,abc则存放在字符串常量池中,所以为false;

  String str3 = str2.intern();由于调用了intern()方法,会返回常量池中的数据,str3此时就指向常量池中的abc,和str1的方式一样,所以为true;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java接口方法默认静态实现代码实例

    Java接口方法默认静态实现代码实例

    这篇文章主要介绍了Java接口方法默认静态实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • Jedis零基础入门及操作Redis中的数据结构详解

    Jedis零基础入门及操作Redis中的数据结构详解

    Jedis 的 API 方法跟 Redis 的命令基本上完全一致,熟悉 Redis 的操作命令,自然就很容易使用 Jedis,因此官方也推荐 Java 使用 Jedis 来连接和操作 Redis
    2022-09-09
  • java学习:日期的运算代码

    java学习:日期的运算代码

    java.util.Date下的很多(构造)方法,已经被标识为"过时"方法,官方推荐使用Calendar类来处理日期的运算,下面是示例:
    2013-02-02
  • IntelliJ IDEA 2020.2.3永久破解激活教程(亲测有效)

    IntelliJ IDEA 2020.2.3永久破解激活教程(亲测有效)

    intellij idea 2022是一款市面上最好的JAVA IDE编程工具,该工具支持git、svn、github等版本控制工具,整合了智能代码助手、代码自动提示等功能,本教程给大家分享IDEA 2022最新永久激活码,感兴趣的朋友参考下吧
    2020-10-10
  • mybatis-plus通用枚举@JsonValue接收参数报错No enum constant

    mybatis-plus通用枚举@JsonValue接收参数报错No enum constant

    最近在使用mybatis-plus时用到了通用枚举,遇到了问题,本文主要介绍了mybatis-plus通用枚举@JsonValue接收参数报错No enum constant,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • 图解Java中插入排序算法的原理与实现

    图解Java中插入排序算法的原理与实现

    插入排序的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。本文将通过图片详解插入排序的原理及实现,需要的可以参考一下
    2022-08-08
  • Spring Boot 初始化运行特定方法解析

    Spring Boot 初始化运行特定方法解析

    这篇文章主要介绍了Spring Boot 初始化运行特定方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • java语言注解基础概念详解

    java语言注解基础概念详解

    这篇文章主要介绍了java语言注解基础概念详解,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • Spring基于xml文件配置Bean过程详解

    Spring基于xml文件配置Bean过程详解

    这篇文章主要介绍了spring基于xml文件配置Bean过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • springboot自动装配的源码与流程图

    springboot自动装配的源码与流程图

    在日常的开发过程中Spring Boot自动装配的特性给我们开发减少了很多重复性的工作,这篇文章主要给大家介绍了关于springboot自动装配的相关资料,需要的朋友可以参考下
    2021-08-08

最新评论