提升JAVA程序的性能

    技术2022-05-11  162

     

    随着时间的推移,Java虚拟机变得越来越好,但是通过一些简单的技巧,你仍然可以明显地改进程序的性能。

     

    简介

    Java的诸多优点已经广为称道。特别是“一次编程,到处运行”的承诺使开发人员可以自由地进行跨平台应用程序的开发而不存在预处理器指令的开销。通常认为Java的弱点在于其性能方面。

    在当前这种认识并不是完全正确的,有很多产品可以提高Java程序的性能并能够使其在很多应用程序中不再成为一个问题。例如,TowerJ是一种将Java字节代码转换成高度优化的本地可执行程序的后期编译器,Jrockit是一种具有自适应优化能力的服务器端的Java虚拟机。尽管如此,运用一些简单的技巧可以使你不必购买上述的这些工具也能够改进Java代码的性能。在文本中我将说明其中的一些。

    本文的讨论主要基于那些高吞吐量的代码(服务器端)。鉴于主要的开销是由那些涉及到对象创建和再创建的GUI代码所引起的,对服务器端代码进行有效的性能估算指针是方法的执行时间。因此,对于所涉及到的示例代码,我记录了执行方法所需的平均时间。记录一个方法的精确执行时间并不是切实可行的,因此我对一系列方法进行计时并计算其平均值。这样做有效地模拟了那些以性能为关键的代码的执行。

    每个示例都带有对字节代码操作进行解释的伪代码。所产生的实际的字节代码可以从CUJWeb站点(www.cuj.com/code)获取。对所有字节代码的解释可以从Javasoft的站点获得。

    改善字符串处理的性能

    C++一样,Java库中定义了自己的String类型。在其外表之下,这种类型是由一个char型数组所实现的,然而使用字符串并不需要理解这一点。NULL符(’/0’)是导致很多学生在学习和使用C++的过程中受挫的祸根;使用Java则不必为此分心,程序员可以专注于应用程序本身以及创建应用程序所用的工具。但是存在着与这种省心的字符串处理方式相关的不利方面,那就是字符串的连接操作符‘+’。

    这个操作符看起来十分有用。多数需要向流写入数据的应用程序都使用‘+’。例如:

    String name = new String("Joe");

    System.out.println(name + " is my name.");

    在上面的代码段中,看起来似乎在println语句中无法作出什么修改以改善其执行的速度。然而,这个语句所产生的字节代码(在此用伪代码表示)却揭示了事实,见程序清单1

     

    清单 1:描述由字符串连接符‘+’所产生的字节代码操作的伪代码

    create new String (STR_1)

    duplicate the String

    load the constant String "Joe" (STR_2)

    call String constructor

    store this String in the local variable array position 0

    get the static out field from the java.io.PrintStream class (OUT)

    create a new StringBuffer (STR_BUF_1)

    duplicate the StringBuffer

    call StringBuffer constructor

    store this StringBuffer in the local variable array position 1

    invoke the append method on STR_BUF_1 with STR_1 as the argument

    load the constant String " is my name." (STR_3)

    invoke the append method on STR_BUF_1 with STR_3 as the argument

    invoke toString method on STR_BUF_1 (STR_4)

    invoke the println method on OUT

     

    这段简单的代码创建了5个对象[1]STR_1, STR_2, STR_3, STR_4, and STR_BUF_1

    需要注意对象创建相对来讲是非常耗费资源的。必须为每个类和类的每一个超类的所有实例变量分配堆存储空间;所有的实例变量必须被初始化;而且类的构造函数和每个超类的构造函数必须被执行。为了创造高效的代码,只限于在绝对必须的情况下进行对象的创建是十分必要的。

    那么,刚才的代码是否可以用一种更高效的方法重写呢?请考虑下面的程序段:

    StringBuffer name = new StringBuffer("Joe");

    System.out.println(name.append(" is my name.").toString());

    相应的字节代码/伪代码请参见程序清单2

     

    清单2:使用StringBufferappend操作符的字节代码/伪代码

    create new StringBuffer (STR_BUF_1)

    duplicate the StringBuffer

    load the constant String "Joe" (STR_1)

    call StringBuffer constructor

    store this StringBuffer in the local variable array position 1

    get the static out field from the java.io.PrintStream class (OUT)

    load STR_BUF_1

    load the constant String " is my name." (STR_2)

    invoke the append method on STR_BUF_1 with STR_2 as the argument

    invoke toString method on STR_BUF_1 (STR_3)

    invoke the println method on OUT

     

    上面的代码值创建了4个对象:STR_1, STR_2, STR_3, STR_BUF_1。你可能会认为减少一个对象的创建并不能起到多大作用。然而,县棉的代码创建了8个对象:

    String name = new String("Joe");

    name+=" is my";

    name+=" name.";

    而这段代码仅创建了5个:

    StringBuffer name = new StringBuffer("Joe");

    name.append(" is my");

    name.append(" name.").toString();

    第二段代码执行的速度比第一段快两倍还多[2]

    结论:使用StringBuffer来改进字符串处理代码的性能。其目标是使新对象的创建达到最少,这可以通过使用在StringBuffer之上使用append方法来代替在String上使用连接操作符来实现。

    更快的日志记录(faster logging

    在我参与开发的每个软件项目中,都要求有一种适当的日志记录机制。在应用程序中包括日志记录功能的原因有很多。主要的原因是为了使维护更加容易。为了在发行的应用程序中实现错误报告,有必要设置开始点。在很多情况下,用户提交的报告含义不清,其描述的问题可能是由多方面因素造成的。如果有一种适当的机制能够使用户收集关于此问题的额外信息,那么解决问题的周期会大大地缩减。

    并没有标准的方法来产生这种信息,通常这有赖于开发人员如何适当地建立这种机制。然而,日志记录机制的实现对应用程序的性能会造成较大的影响。我们的目标是建立一种能够输出有价值的运行时信息但同时使其对运行时性能的影响达到最小的机制。

    避免运行时开销的最显而易见的办法是不将日志记录包括在发行的应用程序中;换句话说,如果执行日志记录的实际代码没有编译到应用程序中的话,那么也就不会对性能造成影响。程序清单3展示了一个定义这样一种记录机制的类。可以对其进行设置使日志记录代码从所产生的字节代码中忽略。这个类将是一个单元素(Singleton)以避免创建不必要的Logger类的实例。

     

    清单3:可以进行配置使其在发布版本中不产生代码的]单元素的Logger

    public class Logger {

      // 这个类的实例

      private static Logger theLogger = null;

      // 调试消息(debug messages)的控制器,设为true时允许调试消息,false则反之

      public static final boolean CAN_DEBUG = true;

      /** 私有的构造函数——只允许一个实例 */

      private Logger() {}

      /** 返回这个类所创建的唯一实例 */

      public static Logger getInstance() {

        if(theLogger == null) { theLogger = new Logger(); }

        return theLogger;

      }

      public void debugMsg(String msg) { System.out.println(msg); }

    }

    正如你所看到的,这个非常简单的类包括一个类型变量,一个类型常量,两个方法和一个构造函数。要使用这个类,只需简单地获取其实例,检查debug是否被启动,并调用debugMsg,如下所示:

    ...

    Logger myLogger = Logger.getInstance();

    ...

    if (Logger.CAN_DEBUG) {

       myLogger.debugMsg("some debug message");

    }

    设想Logger.CAN_DEBUGfalse。当创建应用程序的时候,将会排除死代码同时不会有由此而产生的字节代码。这是因为编译器知道final static变量Logger.CAN_DEBUG总是为false。如果Logger.CAN_DEBUGtrue,那么代码将会被编译并产生相应的字节代码。这样一来,开启了调试消息的编译将会导致产生更多的字节代码。

    这种方法可以扩展到允许对所产生信息的更细微的处理。例如,可以声明一个新的static final的布尔型变量CAN_INFO,还可以实现一个新的public void infoMsg(String msg)方法。

    从性能的角度来看,这是可以使用的最佳方法。几种不同的版本可以相互协作以反映所支持的不同层次的消息。例如,你可以发布一个产品版本和一个调试版本。如果在产品版本中出现了问题,则可以用调试版本与其交换以查明问题出现的所在。

    这种方法的主要缺点在于无法在运行时进行设置,例如将其作为一个系统属性。

    大多数日志记录机制中,主要的性能影响因素在于String对象的创建。这样的话,我们的目标应该是使这种开销达到最小。因此解决方法中需要包括StringBuffer。程序清单4中的Logger类提供了一种可配置的日志记录级别。

     

    清单4:提供可配置日志记录级别Logger

    public class Logger {

      ...

      // 信息消息的控制器

      public static final int CAN_INFO = 1;

      // 调试消息的控制器

      public static final int CAN_DEBUG = 2;

      // 调试级别——缺省为信息消息

      public int LOG_LEVEL = 1;

      ...

      public void setLogLevel(int level) {

        if(level >= 0) { LOG_LEVEL = level; }

      }

      /** 如果CAN_INFO位被设置则返回true */

      public boolean canInfo() {

        if((LOG_LEVEL & CAN_INFO) == CAN_INFO) { return true; }

        return false;

      }

      /** 如果CAN_DEBUG位被设置则返回true */

      public boolean canDebug() {

        if((LOG_LEVEL & CAN_DEBUG) == CAN_DEBUG) { return true; }

        return false;

      }

      public void debugMsg(String msg) { System.out.println(msg); }

      public void infoMsg(String msg) { System.out.println(msg); }

    }

    如上的代码示例提供了一种两级的日志记录方法。它可以处理调试消息和信息消息,还可以很容易地扩展到处理更多的类型。这个类为日志记录机制提供了坚实的基础。

    在应用程序中使用这种实现方法有两种选项。第一是创建一个实现简单API的基类,应用程序将对其进行扩展。第二是由应用程序实现一个定义了简单API的接口。下面是接口的示例:

    public interface LogAPI {

       public void createMsg();

       public void appendLog(String str);

       public void appendLog(int i);

       public void logDebugMsg();

       public void logInfoMsg();

    }

    TestLogger.java(程序清单5)中提供了这个接口的一个实现。

     

    清单5TestLogger.java—定义简单API的接口的示例实现

    /* Copyright (c) 2000 Stepping Stone Software Ltd, John Keyes */

    public class TestLogger implements LogAPI {

      static Logger myLogger = Logger.getInstance();

      StringBuffer msg = new StringBuffer();

      public static void main(String args[]) {

        String strLevel = System.getProperty("app.loglevel");

        if(strLevel != null) {

        int level = Integer.parseInt(strLevel);

        myLogger.setLogLevel(level);

        }

        TestLogger testLog = new TestLogger();

        testLog.test();

      }

      TestLogger() { }

      public void test() {

        int age = 24;

        String name = "Joe";

        if(myLogger.canDebug()) {

          createMsg();

          appendLog(" DEBUG/n Name:");

          appendLog(name);

          appendLog(" Age:");

          appendLog(age);

          logDebugMsg();

        }

        if(myLogger.canInfo()) {

          createMsg();

          appendLog(" INFO/n Name:");

          appendLog(name);

          appendLog(" Age:");

          appendLog(age);

          logInfoMsg();

        }

      }

     

      public void createMsg() { msg.setLength(0); }

      public void appendLog(String str) { msg.append(str); }

      public void appendLog(int i) { msg.append(i); }

     

      public void logDebugMsg() {

        myLogger.debugMsg(msg.toString());

      }

      public void logInfoMsg() {

        myLogger.infoMsg(msg.toString());

      }

    }

    StringBuffer对象的重用是此处的关键。通常你会编写如下的代码作为调试消息:

    debugMsg("Name:" + name + " Age:" + age);

    如前文中所讨论过的,String类型的创建对性能产生不利的影响。如果像TestLogger.java中所示的那样重写,那么获得的性能提升将会是明显的。

    日志记录的级别现在可以使用Logger类中定义的setLogLevel方法在运行时定制。在系统属性的帮助下完成这项工作是一个好主意。你必须定义自己的属性;在本例中它被命名为“app.loglevel”。如果所讨论的问题是一个应用程序,那么你可以对JVM使用-D开关设置“app.loglevel”属性[3]。例如:

    java –D app.loglevel=3 myApp

    另一方面,如果你的程序是一个applet,可以使用<PARAM>标签进行设置:

    <PARAM NAME="app.loglevel" VALUE="2">

    于是,为了设置日志记录级别,你所要做的一切就是获取属性值并对结果调用SetLogLevel方法:

    String strLevel = System.getProperty("app.loglevel");

    If(strLevel != null) {

       int level = Integer.parseInt(strLevel);

       Logger.getInstance().setLogLevel(level);

    }

    这种方法的好处在于:

    l         减少对象创建,也就是说,对象被重用

    l         一个定义良好的API,可以鼓励所有的开发人员遵从一样的标准

    l         可扩展性——单独的开发人员可以对这种事先进行调整以适应其自己的需要

    l         通过使用标准的API减少了维护的开销

    l         可在运行时定制的日志记录级别

    通过自定义集合实现更好的性能

    当需要存储一系列普通对象的时候,最简单的方法通常是使用java.util.Vector。这个类在其被使用的很多情况中的效率较低,其低效率主要有两个原因。第一个原因在于Vector是线程安全的;因此,它的很多方法是同步的。当你总是知道应用程序是单线程的时候,这种同步造成了不必要的开销。Vector效率不高的第二个原因是从中检索对象时所进行的类型转换(casting)数量,如果在Vector中所存储的对象具有相同的类型,就不需要类型转换。这样,为了获得更好的性能,我们需要类型确定的、单线程的集合。

    StringVector.java(程序清单6)是一个String类型集合的示例实现。请记住这个类可用于所有类型的对象。

     

    清单6StringVector.java——String类型集合的示例实现

    /* Copyright (c) 2000 Stepping Stone Software Ltd, John Keyes */

    public class StringVector {

     

      private String [] data;

      private int count;

     

      public StringVector() { this(10); // 缺省尺寸为10 }

     

      public StringVector(int initialSize) {

        data = new String[initialSize];

      }

     

      public void add(String str) {

        // 忽略null字符串

        if(str == null) { return; }

        ensureCapacity(count + 1);

        data[count++] = str;

      }

     

      private void ensureCapacity(int minCapacity) {

        int oldCapacity = data.length;

        if (minCapacity > oldCapacity) {

          String oldData[] = data;

          int newCapacity = oldCapacity * 2;

          data = new String[newCapacity];

          System.arraycopy(oldData, 0, data, 0, count);

        }

      }

     

      public void remove(String str) {

        if(str == null) { return; //忽略null字符串}

        for(int i = 0; i < count; i++) {      

          // 查找匹配

          if(data[i].equals(str)) {

        System.arraycopy(data,i+1,data,i,count-1); //复制数据

             data[--count] = null;

        return;

          }

        }

      }

     

      public final String getStringAt(int index) {

        if(index < 0) { return null; }     

        else if(index > count) {

           return null; // index is > # strings

        }

        else { return data[index]; // index is good }

      }

      // not shown: size(), toStringArray()

    }

    以前你可能在某处见过这样的代码:

    ...

    Vector strings = new Vector();

    strings.add("One");

    strings.add("Two");

    String second = (String)strings.elementAt(1);

    ...

    现在可以将其替换为:

    ...

    StringVector strings = new StringVector();

    strings.add("One");

    strings.add("Two");

    String second = strings.getStringAt(1);

    ...

    其结果是得到改善的性能。TestCollection.java(程序清单7)明显体现了这种性能的差异。StringVector类的add方法的执行时间只有Vector类的add方法的70%左右。而getStringAt方法的执行时间只有Vector类的elementAt方法的25%

     

    清单7TestCollection.java——显著地体现了性能的差异

    /* Copyright (c) 2000 Stepping Stone Software Ltd, John Keyes */

     

    import java.util.Vector;

     

    public class TestCollection {

     

       public static void main(String args []) {

          TestCollection collect = new TestCollection();

     

          if(args.length == 0) {

             System.out.println(

             "Usage: java TestCollection [ vector | stringvector ]");

             System.exit(1);

          }

     

          if(args[0].equals("vector")) {

             Vector store = new Vector();

             long start = System.currentTimeMillis();

             for(int i = 0; i < 1000000; i++) {

                store.addElement("string");

             }

             long finish = System.currentTimeMillis();

             System.out.println((finish-start));

             start = System.currentTimeMillis();

             for(int i = 0; i < 1000000; i++) {

                String result = (String)store.elementAt(i);

             }

             finish = System.currentTimeMillis();

             System.out.println((finish-start));

          }

          else if(args[0].equals("stringvector")) {

             StringVector store = new StringVector();

             long start = System.currentTimeMillis();

             for(int i = 0; i < 1000000; i++) { store.add("string"); }

             long finish = System.currentTimeMillis();

             System.out.println((finish-start));

             start = System.currentTimeMillis();

             for(int i = 0; i < 1000000; i++) {

                String result = store.getStringAt(i);

             }

             finish = System.currentTimeMillis();

             System.out.println((finish-start));

          }

       }

    }

     

    可以对这种技术进行修改以使其能够适应你正在从事的应用程序的要求。例如,你可以创建InterVerctorEmployeeVector等等。

    结束语

    本文决不是关于Java程序性能的“圣经”。其目的是让你知道可以通过对你的Java代码做一些细小的改动来提升其性能。

    相关资源

    TowerJ: http://www.towerj.com/

    Jrockit: http://www.jrockit.com/

    Java虚拟机说明: http://java.sun.com/docs/books/vmspec

    注释

    [1] http://java.sun.com/docs/books/vmspec/2nd-edition.htm/Concepts.doc.htm#19124

    所讲述的:载入一个含有String常量的类或接口可能会创建一个新的String对象表示这个常量。本文假定会创建新的String来表示常量。

    [2] 3执行10万次平均用时578毫秒,例4执行10万次平均用时265毫秒(在我的计算机上测量)

    [3] –D开关被大多数JVM所使用,MicrosoftJview解释器使用 /d:开关。

    关于作者

    John KeyesStepping Stone软件有限公司的资深开发人员。他以前曾在IONA Technologies公司工作。可以通过johnkeyes@yahoo.com与他联系。


    最新回复(0)