新的JavaTM 虚拟机(VMs)具有能够提高性能的特点, 并且你可以使用许多工具来提高应用程序的性能或减小一般类文件的尺寸。这种Java虚拟机的特性和工具可使你在不改变应用程序、或对应用程序仅做很小改动的情况下, 提高应用程序的性能。
Java虚拟机的特性
Java2与过去的版本相比, 性能已有很大提高, 其中包括更快的内存分配、类尺寸的减小、垃圾收集的改善、最新型的监控器和作为标准的内联JIT技术。当使用新的Java2虚拟机时, 你会看到这种性能的改善; 然而, 如果你能够理解速度是如何提高的, 你就能够调整你的应用程序, 以充分挖掘每一点性能潜力。
方法内联
Java虚拟机的Java2版可在运行时自动内联简单方法。在一个未优化的Java虚拟机中,每调用一次新的方法,就创建一个新的堆栈帧(stack frame)。创建一个新的堆栈帧需要一些额外的资源以及该栈的某些再映射(re-mapping),其结果将导致系统开销的少许增加。
由于方法内联可在你的程序中减少方法调用的次数,因而可提高性能。Java虚拟机内联代码内联了返回常数或仅访问内部域(internal fields)的方法。为了利用方法内联,你可以从以下两件事中选做其一;即:你可以使一个方法对虚拟机所要执行的内联看上去是有吸引力的,或者你可以手工内联一个方法,只要它不破坏你的对象模型。在这一语境中的手工内联意味着可以直接从一个方法中将代码移动到正在调用该方法的方法中。
下面的小例子演示了虚拟机的自动方法内联:
public class InlineMe {
int counter=0;
public void method1() {
for(int i=0;i<1000;i++)
addCount();
System.out.println("counter="+counter);
}
public int addCount() {
counter=counter+1;
return counter;
}
public static void main(String args[]) {
InlineMe im=new InlineMe();
im.method1();
}
}
在当前状态,addCount方法对虚拟机中的内联探测器显得是没有吸引力的,因为addCount方法返回一个值。要发现该方法是否被内联:
java -Xrunhprof:cpu=times InlineMe
它生成一个java.hprof.txt输出文件。前十个方法类似下面的结果:
CPU TIME (ms) BEGIN (total = 510) Thu Jan 28 16:56:15 1999
rank self accum count trace method
1 5.88% 5.88% 1 25 java/lang/Character.
2 3.92% 9.80% 5808 13 java/lang/String.charAt
3 3.92% 13.73% 1 33 sun/misc/Launcher$AppClassLoader.getPermissions
4 3.92% 17.65% 3 31 sun/misc/URLClassPath.getLoader
5 1.96% 19.61% 1 39 java/net/URLClassLoader.access$1
6 1.96% 21.57% 1000 46 InlineMe.addCount
7 1.96% 23.53% 1 21 sun/io/Converters.newConverter
8 1.96% 25.49% 1 17 sun/misc/Launcher$ExtClassLoader.getExtDirs
9 1.96% 27.45% 1 49 java/util/Stack.peek
10 1.96% 29.41% 1 24 sun/misc/Launcher.
如果你将addCount方法改变为不再返回一个值,则虚拟机可在运行时将其内联。为使内联代码更友好,应用下面的程序替换addCount方法:
public void addCount() {
counter=counter+1;
再次运行profiler:
java -Xrunhprof:cpu=times InlineMe
这次,java.hprof.txt的输出应该显得是不同的。
AddCount方法已经消失。它已被内联了!
CPU TIME (ms) BEGIN (total = 560) Thu Jan 28 16:57:02 1999
rank self accum count trace method
1 5.36% 5.36% 1 27 java/lang/Character.
2 3.57% 8.93% 1 23 java/lang/System.initializeSystemClass
3 3.57% 12.50% 2 47 java/io/PrintStream.
4 3.57% 16.07% 5808 15 java/lang/String.charAt
5 3.57% 19.64% 1 42 sun/net/www/protocol/file/Handler.openConnection
6 1.79% 21.43% 2 21 java/io/InputStreamReader.fill
7 1.79% 23.21% 1 54 java/lang/Thread.
8 1.79% 25.00% 1 39 java/io/PrintStream.write
9 1.79% 26.79% 1 40 java/util/jar/JarFile.getJarEntry
10 1.79% 28.57% 1 38 java/lang/Class.forName0
新型同步
在Java 2发布之前,同步的方法和对象总是引发一些额外的性能干扰,这是因为用来实现这种代码锁定的机制采用了一种全局监控器注册,它在某些区域仅仅是单线程的(如搜索现存监控器)。在新发布的Java 2中,每个线程都有一个监控器注册,从而消除了许多现存的性能瓶颈。
如果你曾经使用过其它锁定机制来避免同步方法的性能干扰,现在则有必要重访这些代码并考虑新的Java 2新型锁定技术。
在下面的为同步块创建监控器的例子中,你可以将速度提高40%。所用时间在采用JDK1.1.7和采用Sun Ultra 1上的Java 2时分别为14ms和10ms。
class MyLock {
static Integer count=new Integer(5);
int test=0;
public void letslock() {
synchronized(count) {
test++;
}
}
}
public class LockTest {
public static void main(String args[]) {
MyLock ml=new MyLock();
long time = System.currentTimeMillis();
for(int i=0;i<5000;i++ ) {
ml.letslock();
}
System.out.println("Time taken="+
(System.currentTimeMillis()-time));
}
}
Java Hotspot
Java HotSpotTM虚拟机是Sun Microsystem公司的下一代虚拟机。虽然Java HotSpot 虚拟机所采用的规范与Java 2虚拟机所采用的规范相同,但它已被重新设计,并使用了最先进的技术,从而可在未来许多年内,能够为Java平台提供一个强大而可靠的性能引擎。Java HotSpot虚拟机可提供:
可以探测并加速性能关键性代码的实时动态优化技术。
为发挥线程的最大性能而设计的超快速线程同步。
可最快速获取的精确而可靠的垃圾收集器。
由于其简洁、高层次以及面向对象的设计,因而在可维护性和可扩展性方面的重要改进。
JIT(Just-In-Time)编译器
用来提高应用程序性能的最简单的工具是Just-In-Time(JIT)实时编译器。JIT是一个可将Java字节码转换为本地机器码的代码生成器。由JIT调用的Java程序,其运行速度通常要比由解释程序执行字节码时的速度高得多。
JIT编译器首先是在Java开发工具包(JDKTM)1.1.6中作为一种性能更新出现的,而现在它是你在Java 2平台上使用Java解释程序命令时调用的标准工具。你可以使用Java虚拟机的-Djava.compiler=NONE 选项来使JIT编译器失效,这在JIT的末尾部分有更详细的阐述。
JIT编译器是如何工作的?
JIT编译器是作为一种依赖于平台的本地库提供的。如果JIT编译器库存在,则Java虚拟机将初始化Java本地接口(JNI)的本地代码分支以调用在该库中可获得的JIT函数,而不是调用在解释程序中的相应函数。
java.lang.Compiler 类被用来加载本地库并启动JIT编译器内的初始化。当Java虚拟机调用一个Java方法时,它使用在加载的类对象的方法块中所指定的调用(invoker)方法。Java虚拟机具有若干个调用者方法,例如,如果方法是同步的,或者它是一个本地方法,则将使用不同的调用者。JIT编译器使用它自己的调用者。Sun的产品可以为值ACC_MACHINE_COMPILED检查方法存取位以告知解释程序该方法的代码已被编译并存储在加载类中。
代码何时成为JIT编译的代码?
当一个方法被首次调用时,JIT编译器为该方法将方法块编译为本地代码,并将其存储在该方法的代码块中。
一旦代码被编译完成,在Sun平台上所使用的ACC_MACHINE_COMPILED的位则被设定。
我如何知道JIT编译器在做什么?
环境变量JIT_ARGS允许对Sun Solaris JIT编译器进行简单控制。trace 和 exclude(list)是两个有用的值。要从示例InlineMe中排除(exclude)方法并显示跟踪记录(trace),应将JIT_ARGS 做如下设定:
Unix:
export JIT_ARGS="trace exclude(InlineMe.addCount InlineMe.method1)"
$ java InlineMe
Initializing the JIT library ...
DYNAMICALLY COMPILING java/lang/System.getProperty mb=0x63e74
DYNAMICALLY COMPILING java/util/Properties.getProperty mb=0x6de74
DYNAMICALLY COMPILING java/util/Hashtable.get mb=0x714ec
DYNAMICALLY COMPILING java/lang/String.hashCode mb=0x44aec
DYNAMICALLY COMPILING java/lang/String.equals mb=0x447f8
DYNAMICALLY COMPILING java/lang/String.valueOf mb=0x454c4
DYNAMICALLY COMPILING java/lang/String.toString mb=0x451d0
DYNAMICALLY COMPILING java/lang/StringBuffer. mb=0x7d690
>>>> Inlined java/lang/String.length (4)
请注意内联方法(如String.length)是免除的。String.length 也是一个特殊的方法,它一般由Java解释程序编译为一个内部快捷字节码。当使用JIT编译器时,由Java解释程序提供的这些优化失效,从而可以使JIT编译器能够理解哪个方法正在被调用。
如何使用JIT来发挥你的优势
首先要记住的一点是,JIT编译器在第二次调用一个方法时,会获得大部分速度上的改善。JIT编译器的确是编译了整个方法,而不是对其进行逐行解释,逐行解释也是一种在运行一个可执行JIT的应用程序时用以改善性能的途径。这意味着如果代码仅被调用一次,你将不