首页 | 社区 | 博客 | 招聘 | 文章 | 新闻 | 下载 | 读书 | 代码
亲,您未登录哦! 登录 | 注册

从java内核看性能分析与设计

打印文章

分享到:
引言  
java语言自90年代出现以来,因为它的安全性和跨平台性(即所谓的”Write Once,Run Anywhere”)等特点,深得广大程序员的青睐,但是同时,Java程序的运行效率的低下也是程序员的心病。Java是介于解释型和编译型之间的一种语言,同样的程序,如果用编译型语言C来实现,其运行速度一般要比Java快一倍以上。怎样提高java应用程序的效率是广大程序员关心问题。本文将从与Java字节码的运行过程中影响性能的相关因素的分析入手,然后,探讨一些在Java代码的设计过程中具体的有助于提高性能的策略。  
一、性能分析  
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.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)方法  
在多线程访问共享数据时,为了保证数据的一致性,就必然要使用同步技术,但从上面的分析可知,使用同步方法比使用非同步方法的性能要低,因此,我们应尽量少使用同步方法?调用同步方法的代码本身就不需要再同步了。

本栏文章均来自于互联网,版权归原作者和各发布网站所有,本站收集这些文章仅供学习参考之用。任何人都不能将这些文章用于商业或者其他目的。( Pfan.cn )

编程爱好者论坛

本栏最新文章