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

原文URL:http://www.coders.com.cn/dev/detail.asp?Operate_Type=2&bigclassid=2&id=4469

引言  
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等等,这些类里,有些提供了某种排序算法,有的提轨?

你可能感兴趣的:(java,jvm,多线程,数据结构,编程)