java性能优化笔记(三)java程序优化

程序代码优化要点:

  • 字符串优化:分析String源码,了解String常用方法,使用StringBuffer、StringBuilder。
  • List、Map、Set优化:分析常用ArrayList、LinkedList、HashMap、TreeMap、LinkedHashMap、Set接口、集合常用方法优化。
  • 使用NIO:Buffered、Channel操作和原理,使用零拷贝。
  • 引用优化:强引用、弱引用、软引用、虚引用、WeekHashMap。
  • 优化技巧:常用代码优化技巧。这里不一一罗列,请参考下面的详解。

字符串优化:

  • String对象特点:

    • 终态:String类被声明为final,不可被继承重写,保护了String类和对象的安全。在jdk1.5之前final声明会被inline编译,性能大幅度提高,jdk1.5之后性能提升不大。
    • 常量池:String在编译期间会直接分配在方法区的常量池中,当我们写了多个相同值的String对象时,它们实际是指向了同一空间的不同引用罢了。这样对于String这样经常使用的对象访问代价和创建代价是十分低的。需要注意的是当使用String a="123";String b=new String("123");的时候,编译器虽然会创建一个新的String实例,但是实际值依然是指向常量池中的已有的123。我们可以使用a.intern(),String的intern方法返回常量池中的引用,intern是一个native本地方法。
    • 不变性:String对象生成后内存空间永久不会变化,好处是在多线程的情况下不用加锁同步操作。需要注意如下代码:String a="123";a="456";只是改变了对象的引用所指向的位置,实际的”123”是不变的。
  • 关于内存泄漏:
    • 存在内存泄漏的方法:

      String:

      • substring(int,int):

      可以看到substring方法中使用了 return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen)构建截取的新字符串,来看看new String三参的构造函数,

      最后String使用了数组拷贝this.value = Arrays.copyOfRange(value, offset, offset+count);这样做的好处是以空间换取了时间,快速的实现了新字符串的产生。但是当我们构造一个大的字符串进行截取时,并且进行批量截取时,可以想到字节拷贝将会耗费很大内存,存在内存泄漏的问题。这是因为substring使用的三参构造函数返回的字符串被外界调用者保持着强引用,而内存拷贝量大,gc无法回收,所以会产生OutOfMemory异常。

      • new String(char[],int,int):根据上述分析,这个三参的构造函数是罪魁祸首,所以建议不要使用。注意在jdk1.7之前该构造函数是只可以在包内使用的,但是1.7以后变成了公有方法。
      • substring(int):与两参的substring一样,单参的也是调用了new String(char[],int,int),参考如下代码:

        注意jdk1.7之前该函数调用的是两参的substring。

      • concat(String):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。
      • replace(char,char):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。
      • valueOf(char[],int,int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。
      • copyValueOf(char[],int,int):同上。。。
      • toLowerCase(Locale):同上。。。
      • toUpperCase(Locale):同上。。。

      Integer:

      • toString(int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。

      Long:

      • toString(long):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。
    • 修复内存泄漏:解决这个内存泄漏的方法是可以new String(str.substring(0,100));,构造一个新字符串接触了substring原来的强引用,让gc可以正常回收,就不会出现OutOfMemory异常了。至于其他都调用了三参构造函数的,也可以使用new String对返回值重新创建实例解除强引用,也可以自己实现这些函数的功能避免调用String的三参构造函数。
  • 关于字符串分割和查找:
    • String的split:

      split实现中使用了正则表达式,在大量字符串分割时正则表达式会贪婪匹配,效率会降低,不推荐使用。

    • StringTokenizer的使用:

      StringTokenizer是jdk自带的字符串分割工具,由于没有使用正则匹配,所以速度更快,可以参看如下源码:

      StringTokenizer只是使用字符串本身的属性进行了切分。

StringBuffer和StringBuilder:

  • 区别:StringBuffer是线程安全的,所有操作字符串的方法都做了synchronized操作,而StringBuilder没有,是线程不安全的,所以StringBuffer性能低于StringBuilder。
  • 注意事项:StringBuffer和StringBuilder都提供了带有capacity参数的构造函数,主要作用是指定初始化容量(保存字符串缓冲区)的大小,当容量超过capacity时,会进行扩容,扩容为原来大小的2倍,创建新内存空间,同时把原来空间的内存拷贝到新内存空间,然后释放原内存空间。由于内存拷贝很耗时,所以最好指定适当的capacity。
  • 与String的+号对比:当使用+号拼接字符串时,编译器会把+号替换成new StringBuilder().append(),提高拼接效率,但是在大量循环拼接时,编译器不够智能,每次都生成新的StringBuilder,产生大量gc,所以性能不高,最好在循环中使用conact或自己构建StringBuffer或StringBuilder。

List接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-List接口分析》//TODO

Map接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Mapt接口分析》//TODO

Set接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Set接口分析》//TODO

RadnomAccess接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-RadnomAccess接口分析》//TODO

优化集合操作:

  • 分离循环中重复代码:最常见的是循环中调用集合的size()方法,如果集合容量不变,一定要把size提前求出来,int size=list.size();for(int i=0;i<size;i++){...},虽然size()方法返回的是集合的内部变量size,但是由于size()是方法,存在函数的入栈出栈,会耗时。
  • 减少方法调用:与上面一样,方法调用存在函数入栈出栈,所以最好不要在遍历集合中调用方法。
  • 省略相同操作:在遍历集合时我们经常会通过get方法获取集合中的对象再对其操作,如果在遍历中多次使用get也是耗时的,所以可以在循环体内先get出对象存到局部变量中,然后操作局部变量。类似这样的重复操作可以提取出来。
  • 使用迭代器:遍历集合的方法有很多,通过for随机访问,foreach迭代,迭代器迭代等。
    • for随机访问:在对ArrayList时迭代相当快,LinkedList基于链表实现随机访问非常差。
    • 迭代器迭代:迭代器访问集合的速度是最快的,每个集合都实现了Ietrator迭代器接口,每个实现都会根据集合本身特性优化访问数据。
    • foreach迭代:由于foreach会被编译成迭代器,正常理应访问速度快,但是编译后会存在一次对迭代器next()返回变量的多余赋值,所以速度有所减缓。

使用NIO:

  • NIO与传统I/O区别:
  • Buffer和Channel:
  • Buffer原理:
  • API:
  • 零拷贝:

引用类型:

  • 强引用:直接new出的对象都是强引用的,强引用gc回收很少,除非与gc root彻底断开,否则gc宁可抛出OutOfMemory异常。
  • 软引用:软引用会在堆接近阈值的时候被gc回收,只要有足够的内存就会保持引用。使用java.lang.SoftReference构造软引用。
  • 弱引用:软引用的引用级别最低,只要gc线程运行时发现软引用的存在就会回收弱引用,不过gc线程优先级很低,所以也会存活一段时间。使用java.lang.WeekReference构造弱引用。
  • 虚引用:虚引用是无法直接引用的,当使用java.lang.PhantomReference构造虚引用后用get()方法取出原来的强引用时,会直接得到null,因为虚引用get()方法实现直接返回的null。虚引用的唯一作用是配合引用队列回收资源,在gc回收强引用时进入引用队列,在引用队列中通过引用队列的remove()或poll()方法的返回值判断是否被回收,如果回收的话清理其他资源。
  • WeekHashMap:WeakHashMap是HashMap的弱引用版本,里面每个Key的元素都是弱引用的。WeakHashMap继承WeekReference用于把Key放入弱引用中,在get或者put时也会直接或间接调用内部方法expungeStaleEntries(),该方法会检测弱引用是否被回收,如果被回收会释放Key的资源。
  • 引用队列:当对象改变其可达性状态时,对该对象的引用就可能会被置于引用队列(reference queue)中。这些队列被垃圾回收器用来与我们的代码沟通有关对象可达性变化的情况。java.lang.ReferenceQueue,在软引用、弱引用、虚引用构造函数中传入,当gc线程回收时,会把对象放入引用队列,但是它们不会被清除。一旦引用对象被垃圾回收器插人到队列中,其get方法的返回值就肯定会是null,因此该对象就再也不能复活了。
    • public Reference < ? extends下>poll ():用于移除并返回该队列中的下一个引用对象,如果队列为空,则返回null.
    • public Referenceremove ()throws InterruptedException:用于移除并返回该队列中的下一个引用对象,该方法会在队列返回可用引用对象之前一直阻塞。
    • public Referenceremove (long timeout) throws interrupte-dException:用于移除并返回队列中的下一个引用对象。该方法会在队列返回可用引用对象之前一直阻塞,或者在超出指定超时后结束。如果超出指定超时,则返回null.如果指定超时为0,意味着将无限期地等待。

代码优化技巧:

  • 异常优化:永远不要在循环中处理异常,循环构造异常栈会十分耗时,把异常捕获放循环外面。
  • 使用局部变量:局部变量存放在虚拟机栈的本地变量表中,本地变量表会随着方法销毁(出栈)而销毁,所以不需要gc。new出的对象存放在堆中,需要gc回收。而static变量存放于方法区,在编译时通过cinit构造生成,所以生命周期与类相同,方法区gc几乎不去回收(永久代),所以static多了会很耗费内存。
  • 位运算:位运算速度是最快的,经常使用的除法可替换成>>,乘法可替换成<<,右移一位等同于除以2,左移一位等同于乘以2。
  • 替换switch:if和switch性能区别并不大,但是有时使用if性能会更高,比如:
switch(num):
case 1:return 1;
case 2:return 2;
case 3:return 3
default:return -1

使用if优化后:

int swArr[3]={1,2,3};
if(num<1||num>3){
    return -1;
}else{
    return swArr[num];
}

由于对数组随机访问非常快,所以使用if要比switch快。这需要根据不同业务选择性优化。另外,使用策略或者工厂模式都可以优化swtich和if判断,方便解耦。

  • 表达式:表达式运算是耗时的,可以在不影响业务的情况下把一些循环内的重复性的表达式提取到循环外用变量保存,然后再在循环内部使用。另外我们经常使用24*60*60这样的方式计算一天的秒数,其实可以在变量中直接写好计算结果。
  • 展开循环:展开循环可参考如下代码:

未展开前:

int num[]=new int[10000];
for(int i=0;i<10000;i++){
    num[i]=i;
}

展开后:

int num[]=new int[10000];
for(int i=0;i<10000;i+=3){
    num[i]=i;
    num[i+1]=i+1;
    num[i+2]=i+2;
}

这种情况展开后要比展开前运算速度快,因为循环时减少了步进的判断。

  • 使用布尔运算代替位运算:位运算虽然快,也存在位逻辑,但是在判断时使用位运算和其他逻辑运算一起时,java的if会完成位运算的判断执行后再继续判断条件中的其他逻辑运算。而布尔运算在条件满足后会直接跳转到if块中执行,省略后续的逻辑运算。不过通常我们只用布尔运算。
  • 优化数组拷贝:使用System.arrayCopy(),因为他是native的,调用操作系统实现的拷贝,效率非常高。
  • 使用缓冲区:BufferedInput和BufferedOutput在上面的文章中已经介绍过了,同样BufferedWrtier和BufferedReader效率也非常高。优先使用缓冲区。
  • 使用静态方法:静态方法不需要构建实例就可以直接使用,并且由于方法区gc很少回收,且jvm会缓存常用的类,所以一些常用工具类封装成static的性能会更高。而且要比函数重载更具有表达意义。
  • 使用设计模式:在对象比较大时可以使用原型模式替换new操作,尤其对象构造函数比较耗时时,可以直接使用原型模式clone对象,也可以使用apache的commons下的BeanUtil中的clone方法。同样在一些业务下,可以使用单例模式、享元模式、代理模式、工厂模式等常用的设计模式优化对象生成过程,提升性能。
时间: 2024-08-05 18:18:23

java性能优化笔记(三)java程序优化的相关文章

Java基础学习笔记三 Java基础语法

Scanner类 Scanner类属于引用数据类型,先了解下引用数据类型. 引用数据类型的使用 与定义基本数据类型变量不同,引用数据类型的变量定义及赋值有一个相对固定的步骤或格式. 数据类型 变量名 = new 数据类型(); 每种引用数据类型都有其功能,我们可以调用该类型实例使用其功能. 变量名.方法名(); Scanner类 Scanner类可以完成用户键盘录入,获取到录入的数据. Scanner使用步骤: 导包: import java.util.Scanner; 创建对象实例:Scann

JAVA 虚拟机深入研究(三)——Java内存区域

JAVA 虚拟机深入研究(一)--关于Java的一些历史 JAVA 虚拟机深入研究(二)--JVM虚拟机发展以及一些Java的新东西 JAVA 虚拟机深入研究(三)--Java内存区域 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的围城,城外的人想进去,城里的人想出来. Java运行的时候会把内存分为若干个,他们各有各的用途,每块区域的创建和销毁都是相对独立的,有的跟虚拟机一起混,有的则抱着用户的大腿同生共死. 按照第七版的<Java虚拟机规范>规定,JVM所管理的内存包括以下

CentOS7.4—Apache优化应用三(网页优化)

Apache优化应用三(网页优化)目录第一部分 准备工作第二部分 安装Apache服务第三部分 Apache网页优化-网页压缩第四部分 Apache网页优化-网页缓存 第一部分 准备工作一:服务器:Linux系统-CentOS 7.4:IP地址:192.168.80.10 客户端:以WIN7为例,测试验证结果,与服务器在同一网段:IP地址:192.168.80.2 二:准备压缩包 //apr-1.6.2.tar.gz和apr-util-1.6.0.tar.gz是httpd2.4以后的版本所需要的

性能优化系列三:JVM优化1

一.几个基本概念 GCRoots对象都有哪些 所有正在运行的线程的栈上的引用变量.所有的全局变量.所有ClassLoader... 1.System Class.2.JNI Local3.JNI Global4.Thread Block5.Busy Monitor6.Java Local7.Native Stack8.Unfinalized9.Unreachable10.Java Stack Frame11.Unknown 栈帧的解释 Java虚拟机栈(Java Virtual Machine

Java基础学习笔记一 Java介绍

java语言概述 Java是sun公司开发的一门编程语言,目前被Oracle公司收购,编程语言就是用来编写软件的. Java的应用 开发QQ.迅雷程序(桌面应用软件) 淘宝.京东(互联网应用软件) 安卓应用程序 Java的擅长 互联网:电商.P2P等等 企业级应用:ERP.CRM.BOS.OA等等 Java语言平台 JavaSE(标准版)部分,JavaSE并不能开发大型项目. JavaEE(企业版)部分,学习完JavaEE部分就可以开发各种大型项目了. java语言开发环境 JDK是Java开发

Java基础学习笔记十 Java基础语法之final、static、匿名对象、内部类

final关键字 继承的出现提高了代码的复用性,并方便开发.但随之也有问题,有些类在描述完之后,不想被继承,或者有些类中的部分方法功能是固定的,不想让子类重写.可是当子类继承了这些特殊类之后,就可以对其中的方法进行重写,那怎么解决呢?要解决上述的这些问题,需要使用到一个关键字final,final的意思为最终,不可变.final是个修饰符,它可以用来修饰类,类的成员,以及局部变量. final的特点 final修饰类不可以被继承,但是可以继承其他类. class Yy {} final clas

【Java基础学习笔记】Java中Socket+Swing设计简单通信

在<Java从入门到精通(第3版)>的原书中,客户端仅能发送一次数据,我在此基础上修改了一点点,实现了多次发送数据的单向通讯. 1. 服务器端 package Tcp_IP; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.sq

java jvm学习笔记三(class文件检验器)

欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 前面的学习我们知道了class文件被类装载器所装载,但是在装载class文件之前或之后,class文件实际上还需要被校验,这就是今天的学习主题,class文件校验器. class文件 校验器,保证class文件内容有正确的内部结构,java虚拟机的class文件检验器在字节码执行之前对文件进行校验,而不是在执行中进行校验class文件校验器要进行四趟独立的扫描来完成校验工作 class文件校验器分成四趟独立的扫描来完

Java泛型学习笔记 - (三)泛型方法

泛型方法其实和泛型类差不多, 就是把泛型定义在方法上, 格式大概就是: public <类型参数> 返回类型 方法名(泛型类型 变量名) {...}泛型方法又分为动态方法和静态方法,:1. 动态泛型方法其实在前一篇博文中我已经用到了, 1 public class Box<T> { 2 3 private T obj; 4 5 public Box() {} 6 7 public T getObj() { 8 return obj; 9 } 10 11 public void se

Java泛型读书笔记 (三)

泛型对于老代码的支持 Java的泛型设计成类型擦除的目的,很大一部分是为了兼容老老代码.如下的一段代码: void setLabelTable(Dictionary table) table的类型是非泛型的Dictionary,但是我们可以传入泛型的Dictionary: Dictionary<Integer, Component> labelTable = new Hashtable<>(); labelTable.put(0, new JLabel(new ImageIcon(