一起学习
引言
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)的,而不是每个实例中变量里都存放那个对象的引用。
下载本文示例代码
从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计从java内核看性能分析与设计