性能优化总结:CPU和Load、NIO以及多线程

引言
Java语言自90年代出现以来,由于它的安全性和跨平台性(即所谓的”Write Once,Run Anywhere”)等特色,深得广大程序员的青睐,可是同时,Java程序的运行效率问题也是程序员的心病。Java是介于解释型和编译型之间的一种语 言,一样的程序,假如用编译型语言C来实现,其运行速度通常要比Java快一倍以上。怎样提升java应用程序的效率是广大程序员关心问题。本文将从与 Java字节码的运行过程当中影响性能的相关因素的分析入手,而后,探讨一些在Java代码的设计过程当中具体的有助于提升性能的策略。

1、性能分析


JVM运行时的负载主要集中在字节码的执行,内存治理,线程治理和其余的操做几个方面。

1.1 JVM的结构
JVM中运行的是Java字节码(Bytecode).class文件,这种class文件除了准肯定义一个类或接口的表示外,还定义了一些与平台相关的诸如字节顺序的具体信息。
Java的数据类型分为primitive和reference,对于不一样的数据类型的运算在JVM中的有不一样的指令去执行,好比 iadd,ladd,fadd就是分别针对int,long,float的加法运算,固然,它们的执行效率也不同, 运行时的数据区,在一个程序运行时,JVM都要为它定义不一样的运行数据区,有些数据区在JVM启动时就建立好了,直到整个JVM退出时才释放掉,还有一些 数据区的是属于每一个线程的,它的生命周期与线程相等。
JVM中的逻辑结构有:
PC(program counter)寄存器 ,每一个线程有本身的PC(program counter)寄存器,当JVM执行的方法不是本地(Native)的时,这里存放当前线程运行的指令的地址,假如是本地(Native)的,PC(program counter)寄存器的值没有定义。
JVM栈(stack) ,当建立线程时,每一个线程都建立一个属于本身的栈,用来存放frames(见下面),它存有本地变量,方法调用中的部分结果。
堆(heap) ,JVM中全部线程共享这个堆,类的实例和数组都是从堆中分配内存的,堆是在整个JVM启动时初始化的。
方法区(Method Area) ,线程间共享,它存放每一个类中的运行时常数池(runtime constant pool),域值和方法数据,以及方法和类的构造函数的代码,其中包括用于类的非凡方法,实例初始化和接口类型的初始化,
运行时常数池(runtime constant pool) ,是每一个类或接口的class文件中的常数池表在运行时的表示,它包括各类常数如编译时就知道的数字常量,还有运行时才能肯定的方法和域的引用,相似传统语言的符号表,
本地(Native )方法栈(Stack) ,用来支持本地(Native)方法调用,这些方法用非Java的语言编写,须要传统的"C"栈。
帧(Frames) ,存放方法调用中的数据和部分结果及返回值,执行动态链接,分派例外,一个新的Frame在方法被调用时建立,方法调用正常或非正常完成时销 毁,Frame从每一个线程建立的JVM的栈中分配内存,它属于每一个线程,每一个Frame有本身的本地变量组,本身的操做栈(Operand Stack)和指向当前方法的运行时常数池的引用,本地变量组和操做栈的大小在编译的时候就已经肯定,在一个得到控制的线程中只有一个Frame是激活 的,这个Frame为当前Frame,它的方法为当前方法,方法所属的类为当前类,当这个方法又调用别的方法或结束时,这个当前Frame再也不激活,一个 新的Frame被建立并成为当前Frame,直到当前方法调用完成后,这个Frame被释放并返回结果,前一个方法的Frame成为当前的 Frame,
本地变量 ,每一个方法的Frame包含一组在方法中定义的本地变量,它们的大小在Java编译时就已肯定。
动态链接(Dynamic Linking) ,每一个Frame包含一个指向当前方法的运行时常数池的引用,它经过符号引用(symbolic references)访问变量和指向被引用的方法,动态链接(Dynamic Linking)在运行时将这些方法的符号引用转为具体的方法引用,并加载相应的类,它还将变量影射到当前运行时的变量的内存偏移上。

1.2 字节码(Bytecode)的执行
JVM动态地加载(Loads),链接(Links)和初始化(Initializes)类和接口的字节码,加载(Loading)就是JVM发现具备某 一特定名字的类或接口的二进制表示,并从这个二进制表示在内存中建立出一个类或接口,链接(Linking)就是使一个类或接口与JVM的运行时状态很好 的结合,以便执行它,一个类或接口的初始化就是执行它的初始化方法。

1.3 内存治理
Java是一个面向对象的语言,所以,在JVM的内存中大部分是对象,从上面的分析咱们知道,对象的内存是从堆(heap)分配的,对象内存的回收是由自 动内存治理系统(由叫垃圾收集器-Garbage Collector)来完成的,编程人员是不用显式的释放内存的,垃圾收集器Garbage Collector经过记录指向对象的引用的数目来决定是否释放对象所占据的内存空间,当指向某个对象的引用数为零时,这个对象就能够释放了。

1.4 线程治理
Java是一个支持多线程的语言,所以线程的治理是JVM的一个主要工做,每一个线程都有本身的工做内存,线程间的共享变量是存放在整个JVM的主内存中 的,线程间数据的同步经过lock来共享数据并保证数据的一致性,线程间控制的转移经过对wait,notify等方法的调用来实现。

2、性能设计

经过以上的分析,咱们就如下几个方面提出一些有关性能设计的策略。

2.1 对象的构造
从上面咱们知道,Java对象的内存是自动治理的,所以,通常认为,程序员是不用担忧内存的分配的,但这种想法是不彻底正确的,java经过垃圾收集器 (Garbage Collector)来处理内存分配与释放的底层操做,程序员不用直接治理内存,这样防止了因为内存的错误操做致使的数据破坏(corruption), 但并不意味着程序员不用担忧内存的使用,内存的使用不但会给系统带来很大的负担,好比,Java并不阻止程序占用过多的内存,当对象向堆所请求的内存不足 时,垃圾收集器(Garbage Collector)就会自动启动,释放那些引用数为零的对象所占用的内存,Java也不会自动释放无用的对象的引用,假如程序忘记释放指向对象的引用, 则程序运行时的内存随着时间的推移而增长,发生所谓内存泄漏(memory leaks),建立对象不但消耗CPU的时间和内存,同时,为释放对象内存JVM需不停地启动垃圾收集器(Garbage Collector),这也会消耗大量的CPU时间。
策略:尽可能避免在被常常调用的代码中建立对象。
对于集合类(collection),应尽可能初始化它的大小,假如不初始化它的大小,JVM自动给它一个缺省的大小,当你的要求大于这个缺省的大小时,JVM就会从新建立一个新的collection对象,原来的对象就释放掉,这样必然会增长JVM的负担。
当一个类的多个实例在其本地的变量里访问一个特定的对象时,最好将这个变量设计为静态(static)的,而不是每一个实例中变量里都存放那个对象的引用。
由于对象的建立是很是昂贵的,因此应尽可能重用,少用new来得到对象的引用,尽可能重用容器对象(Vector,Hashtable)等而不老是建立新的对象抛弃旧的对象,但必定要注重释放容器对象中所保存的指向别的对象的引用。
尽可能使用primitive数据类型。
当只是访问一个类的某个方法时,不要建立该类的对象,而是将该方法设计成一个static的方法。
尽可能简化类的继续关系和设计简单的构造函数。
建立简单数据类型的数组要比初始化一个这样的数组快,建立一个复杂类型的数组要比克隆一个这样的数组快。

2.2 字符串(String)
String在Java程序中被普遍使用,String对象是不可改变的,例如: String str="testing"; str=str+"string"; 这个"testing"String一旦建立,就不能更改,但指向这个String的引用str能够改变,str原来指向"testing",通过第二个 运算后,改成指向新的String"testingstring"了。针对String的这个特性,对于String的使用,咱们有以下策略:
假如字符串在程序中可能被改变,好比增长,接或删除字符,就应使用StringBuffer,建立具备初始大小的StringBuffer对象,尽可能重用该对象,而不使用"+"操做。
当咱们要分析字符串中的字符时,就不要使用String或StringBuffer,而是使用字符(cbar)数组,别是在循环中分析字符时,更应如此。 
尽可能少用StringTokenizer,它的方法的性能比较差。

2.3 输入输出(Input/Output)
程序的I/O每每是性能的瓶颈所在,java io定义了两个基本的抽象类:InputStream和OutputStream,对于不一样的数据类型好比磁盘,网络又提供了不一样的实现,java.io 也提供了一些缓冲流(Buffered Stream),使硬盘能够很快的读写一大块的数据, 而Java基本的I/O类一次只能读写一个字节,但缓冲流(Buffered Stream)能够一次读写一批数据,,缓冲流(Buffered Stream)大大提升了I/O的性能,对象的序列化(serialization)是一个将处于生成期的对象序列化成能够在流(stream)中读写的 数据的过程,象的序列化是一个很是复杂,昂贵的过程,要一个类implements接口 java io Serializable,它就能够被自动的序列化,针对以上分析,咱们对I/O有以下对策:
·小块小块的读写数据会很是慢,所以,尽可能大块的读写数据
·使用BufferedInputStream和BufferedOutputStream来批处理数据以提升性能
·对象的序列化(serialization)很是影响I/O的性能,尽可能少用
·对不需序列化的类的域使用transient要害字,以减小序列化的数据量

2.4 循环(Loop)
由于循环中的代码会被反复的执行,因此循环中常常是寻找有关性能问题的地方,嵌套的循环更轻易产生性能问题, 在循环中,咱们应该注重以下问题:
·循环常量(Loop Constant),在循环中它的值不会改变,所以,它的值应该在循环外先计算出来。
·本地变量(Local Variable),从上面的分析可知,在方法中使用本地变量比使用对象的属性消耗较少的资源,在循环中却不同, 由于循环中的代码要反复地被运行,所以,尽可能少地在循环中建立对象和变量。
·尽早结束循环,假如循环体在知足必定条件就能够结束,就应尽快结束。

2.5 集合类(Collections)
集合类在此Java编程中被普遍地使用,大体上,一个集合类就是将一组对象组装成一个对象,Java的集合类框架由一些接口和一些为通用目的而实现 (implementation)的类组成,集合类的基本结构由六个在java.util包内的接口组成,主要有以下结构:
Collection 这是集合类的基本接口,它为一组对象提供了一些简单的方法,
List 具备能够控制的顺序,但并无定义或限制按什么排序。
Set 不能包含重复的元素,
Map 将一个键(Key)影射到一个值(Value),不答应有重复的键,
除了上述接口以外,java.util还提供了一些为通用目的而实现的类,如Vector,ArrayList,Hashtable等等,这些类里,有些 提供了某种排序算法,有的提供了同步的方法,有如此多的集合类,在具体使用过程当中,咱们如何根据本身的须要选择合适的集合类,将对程序的性能产生很大的影 响,下面将一些经常使用的类进行比较, Vector和ArrayList Vector和ArrayList在使用上很是类似,均可用来表示一组数量可变的对象应用的集合,而且能够随机地访问其中的元素。
它们的区别以下:
Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,因为线程的同步必然要影响性能,所以,ArrayList的性能比Vector好。
当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增长50%的大小,这样,ArrayList就有利于节约内存空间。
Hashtable和HashMap
它们的性能方面的比较相似 Vector和ArrayList,好比Hashtable的方法是同步的,而HashMap的不是。
ArrayList和LinkedList
对于处理一列数据项,Java提供了两个类ArrayList和LinkedList,ArrayList的内部实现是基于内部数组Object[],所 以从概念上讲,它更象数组,但LinkedList的内部实现是基于一组链接的记录,因此,它更象一个链表结构,因此,它们在性能上有很大的差异。
(1)从上面的分析可知,在ArrayList的前面或中间插入数据时,你必须将其后的全部数据相应的后移,这样必然要花费较多时间,因此,当你的操做是 在一列数据的后面添加数据而不是在前面或中间,而且须要随机地访问其中的元素时,使用ArrayList会提供比较好的性能。
(2)而访问链表中的某个元素时,就必须从链表的一端开始沿着链接方向一个一个元素地去查找,直到找到所需的元素为止,因此,当你的操做是在一列数据的前面或中间添加或删除数据,而且按照顺序访问其中的元素时,就应该使用LinkedList了。
(3)假如在编程中,1,2两种情形交替出现,这时,你能够考虑使用List这样的通用接口,而不用关心具体的实现,在具体的情形下,它的性能由具体的实现来保证。
设置集合类的初始大小
在Java集合框架中的大部分类的大小是能够随着元素个数的增长而相应的增长的,咱们彷佛不用关心它的初始大小,但假如咱们考虑类的性能问题时,就必定要 考虑尽量地设置好集合对象的初始大小,这将大大提升代码的性能,好比,Hashtable缺省的初始大小为101,载入因子为0.75,即假如其中的元 素个数超过75个,它就必须增长大小并从新组织元素,因此,假如你知道在建立一个新的Hashtable对象时就知道元素的确切数目如为110,那么,就 应将其初始大小设为110/0.75=148,这样,就能够避免从新组织内存并增长大小。
2.6 方法(Methods) 从上面的JVM的结构分析能够看出,Java程序在执行的过程当中就是一个初始化对象和调用其方法过程,其中对方法的调用花费了不少资源,这些资源都用来转移线程控制,传递参数,返回结果和建立用于存放本地变量及中间结果的帧栈(stack frame)。 代码嵌入(Inlining) 因为方法的调用须要消耗大量的资源,所以,Java编译器能够将一些方法调用转化为代码嵌入(Inlining),就是将一段代码对一个方法的调用转化为 将该方法的代码在编译时嵌入到调用处,这样,因为减小了方法的调用,就能够大大提升代码的性能,当将一个方法声明为 final,static,private时,编译器就会自动的使用代码嵌入技术将该方法代码在编译时嵌入到调用处。 同步(Synchronized)方法 在多线程访问共享数据时,为了保证数据的一致性,就必然要使用同步技术,但从上面的分析可知,使用同步方法比使用非同步方法的性能要低,所以,咱们应尽可能少使用同步方法?调用同步方法的代码自己就不须要再同步了。
相关文章
相关标签/搜索