1、 J2EE 平台架构性能优化方案应用 J2EE 平台开发的系统的性能是系统使用者和开发者都关注的问题,本文从服务器端编程时应注意的几个方面讨论代码对性能的影响,并总结一些解决的建议。关键词:性能,Java,J2EE,EJB,Servlet,JDBC 一、概要Java 2 Platform, Enterprise Edition (J2EE)是当前很多商业应用系统使用的开发平台,该技术提供了一个基于组件的方法来设计、开发、装配和部署企业级应用程序。J2EE 平台提供了一个多层结构的分布式的应用程序模型,可以更快地开发和发布的新的应用解决方案。 J2EE 是一种技术规范,定义了整个标准的应用开发体
2、系结构和一个部署环境,应用开发者开发时只要专注于具体商业逻辑和商业业务规则的实现上,而其他的诸如事务、持久化、安全等系统开发问题可以由应用程序容器或者服务器处理,开发完成后,就可以方便地部署到实现规范的应用服务器中。 作为网络上的商业应用系统,同时访问的人数是很多的,在大量访问的情况下,过多的资源请求和有限的服务器资源(内存、CPU 时间、网络带宽等)之间就会出现矛盾,应用系统的性能就显得很重要了,有时正确的代码并不能保证项目的成功,性能往往是最后决定一个项目是否成功关键。本文主要从性能的角度出发,讨论 J2EE 服务器端的代码性能优化和提升。 二、常见的 Java 编程 J2EE 语言基础是
3、 Java,常用的 Java 代码问题对应用系统的性能影响,下面讨论了一些应该注意方面。 使用 StringBuffer 代替 String 当处理字符串的相加时,常见的写法是:String str1 = “Hello“;String str2 = “welcome to world“;String str3 = str1 + “, “ + str2 +“!“;System.out.println(str3);很多人都知道,这样的代码效率是很低的,因为 String 是用来存储字符串常量的,如果要执行“”的操作,系统会生成一些临时的对象,并对这些对象进行管理,造成不必要的开销。如果字符串有连接
4、的操作,替代的做法是用 StringBuffer 类的 append 方法,它的缺省构造函数和 append 的实现是:public StringBuffer() / 构造函数this(16); / 缺省容量 16 public synchronized StringBuffer append(String str) if (str = null) str = String.valueOf(str); int len =str.length(); int newcount = count + len; if(newcount value.length) expandCapacity(newco
5、unt); / 扩充容量str.getChars(0, len, value, count); count = newcount; return this; 当字符串的大小超过缺省 16 时,代码实现了容量的扩充,为了避免对象的重新扩展其容量,更好的写法为:StringBuffer buffer = new StringBuffer(30); / 分配指定的大小。buffer.append(“hello“); buffer.append(“,“); buffer.append(“welcometo world!“); String str = buffer.toString();生成对象时,分
6、配合理的空间和大小Java 中的很多类都有它的默认的空间分配大小,对于一些有大小的对象的初始化,应该预计对象的大小,然后使用进行初始化,上面的例子也说明了这个问题,StringBuffer创建时,我们指定了它的大小。另外的一个例子是 Vector,当声明 Vector vectnew Vector()时,系统调用:public Vector() / 缺省构造函数this(10); / 容量是 10;缺省分配 10 个对象大小容量。当执行 add 方法时,可以看到具体实现为: public synchronized boolean add(Object o) modCount+;ensureCa
7、pacityHelper(elementCount+1);elementDataelementCount+ =o;return true;private void ensureCapacityHelper(int minCapacity) int oldCapacity = elementData.length; if (minCapacity oldCapacity) Object oldData = elementData; int newCapacity = (capacityIncrement 0) ? (oldCapacity + capacityIncrement) : (oldC
8、apacity * 2); if (newCapacity size; i+).如果 size=1000,就可以减少 1000 次 size()的系统调用开销,避免了循环体重复调用。再看如下的代码片: for (int i = 0;i 0 )value =object.getValue();可以修改为:int value;if(i0 )NewObject object = new NewObject(); Value =object.getValue(); 另外,应该尽量重复使用一个对象,而不是声明新的同类对象。一个重用对象的方法是改变对象的值,如可以通过 setValue 之类的方法改变对象
9、的变量达到重用的目的。变量的注意事项尽量使用局部变量,调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack) 中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。尽量使用静态变量,即加修饰符 static,如果类中的变量不会随他的实例而变化,就可以定义为静态变量,从而使他所有的实例都共享这个变量。方法(Method)调用在 Java 中,一切都是对象,如果有方法(Method)调用,处理器先要检查该方法是属于哪个对象,该对象是否有效,对象属于什么类型,然后选择合适的方法并调用。可以减少方法的调用,同样一个方法: public void CallM
10、ethod(int i )if( i =0 )return;. / 其他处理如果直接调用,int i = 0; . CallMethod(i);就不如写成:int i = 0; . if( i =0 ) CallMethod(i); 不影响可读性等情况下,可以把几个小的方法合成一个大的方法。另外,在方法前加上 final,private 关键字有利于编译器的优化。慎用异常处理 异常是 Java 的一种错误处理机制,对程序来说是非常有用的,但是异常对性能不利。抛出异常首先要创建一个新的对象,并进行相关的处理,造成系统的开销,所以异常应该用在错误处理的情况,不应该用来控制程序流程,流程尽量用 wh
11、ile,if 等处理。在不是很影响代码健壮性的前提下,可以把几个 try/catch 块合成一个。同步同步主要出现在多线程的情况,为多线程同时运行时提供对象数据安全的机制,多线程是比较复杂话题,应用多线程也是为了获得性能的提升,应该尽可能减少同步。另外,如果需要同步的地方,可以减少同步的代码段,如只同步某个方法或函数,而不是整个代码。使用 Java 系统 APIJava 的 API 一般都做了性能的考虑,如果完成相同的功能,优先使用 API 而不是自己写的代码,如数组复制通常的代码如下: int size = 1000;String strArray1 = new Stringsize;Str
12、ing strArray2 = new Stringsize;for(inti=0;isize;i+) / 赋值strArray1i = (new String(“Array: “ + i);for(inti=0;isize;i+) / 复制strArray2i=(new String(String)ai);如果使用 Java 提供的 API,就可以提高性能:int size = 1000;String strArray1 = new Stringsize;String strArray2 = new Stringsize;for(inti=0;isize;i+) / 赋值strArray1i
13、 = (new String(“Array: “ + i);System.arraycopy(strArray1,0,strArray2,0,size); / 复制同样的一个规则是,当有大量数据的复制时,应该使用 System.arraycopy()。 三、I/O 性能输入/输出( I/O)包括很多方面,我们知道,进行 I/O 操作是很费系统资源的。程序中应该尽量少用 I/O 操作。使用时可以注意: . 合理控制输出函数System.out.println()对于大多时候是有用的,特别是系统调试的时候,但也会产生大量的信息出现在控制台和日志上,同时输出时,有序列化和同步的过程,造成了开销。特别
14、是在发行版中,要合理的控制输出,可以在项目开发时,设计好一个 Debug 的工具类,在该类中可以实现输出开关,输出的级别,根据不同的情况进行不同的输出的控制。使用缓存读写内存要比读写文件要快很多,应尽可能使用缓冲。尽可能使用带有 Buffer 的类代替没有 Buffer 的类,如可以用 BufferedReader 代替 Reader,用 BufferedWriter 代替 Writer 来进行处理 I/O 操作。同样可以用 BufferedInputStream 代替 InputStream 都可以获得性能的提高。四、ServletServlet 采用请求响应模式提供 Web 服务,通过 S
15、ervletResponse 以及ServletRequest 这两个对象来输出和接收用户传递的参数,在服务器端处理用户的请求,根据请求访问数据库、访问别的 Servlet 方法、调用 EJB 等等,然后将处理结果返回给客户端。尽量不使用同步Servlet 是多线程的,以处理不同的请求,基于前面同步的分析,如果有太多的同步就失去了多线程的优势了。 不用保存太多的信息在 HttpSession 中很多时候,存储一些对象在 HttpSession 中是有必要的,可以加快系统的开发,如网上商店系统会把购物车信息保存在该用户的 Session 中,但当存储大量的信息或是大的对象在会话中是有害的,特别是
16、当系统中用户的访问量很大,对内存的需求就会很高。具体开发时,在这两者之间应作好权衡。清除 Session通常情况,当达到设定的超时时间时,同时有些 Session 没有了活动,服务器会释放这些没有活动的 Session, 不过这种情况下,特别是多用户并访时,系统内存要维护多个的无效 Session。当用户退出时,应该手动释放,回收资源,实现如下: HttpSession theSession = request.getSession(); / 获取当前 Sessionif(theSession != null) theSession.invalidate(); / 使该 Session 失效五
17、、EJB 问题EJB 是 Java 服务器端服务框架的规范,软件厂商根据它来实现 EJB 服务器。应用程序开发者可以专注于支持应用所需的商业逻辑,而不用担心周围框架的实现问题。EJB 规范详细地解释了一些最小但是必须的服务,如事务,安全和名字等。缓存 Home 接口EJB 库使用 Enterprise Bean 的客户端通过它的 Home 接口创建它的实例。客户端能通过 JNDI 访问它。服务器通过 Lookup 方法来获取。JNDI 是个远程对象,通过 RMI 方式调用,对它的访问往往是比较费时的。所以,在设计时可以设计一个类专门用来缓存 Home 接口,在系统初始化时就获得需要的 Home
18、接口并缓存,以后的引用只要引用缓存即可。封装 Entity Bean直接访问 Entity Bean 是个不好的习惯,用会话 Bean 封装对实体 Bean 的访问能够改进事务管理,因为每一个对 get 方法的直接调用将产生一个事务,容器将在每一个实体Bean 的事务之后执行一个“Load-Store” 操作。最好在 Session Bean 中完成 Entity Bean 的封装,减少容器的事务处理,并在Session Bean 中实现一些具体的业务方法。释放有状态的 Session Bean相当于 HttpSession,当把一个 Session Bean 设为 Stateful,即有状态
19、的Session Bean 后,应用容器(Container)就可能有“钝化”(Passivate)和活化(Activate)过程,即在主存和二级缓存之间对 SessionBean 进行存储位置的转移,在这个过程中,存在序列化过程。通常有状态 Session Bean 的释放是在超时时发生,容器自动的清除该对象,但是如果交给容器管理,一方面可能产生对象钝化,另一方面未超时期间,系统还要 维护一份该对象,所以如果我们确认使用完该 StatefulSession Bean 后不再需要时,可以显式的将其释放掉,方法是调用:theSesionBean.remove();六、数据库访问在 J2EE 开发
20、的应用系统中,数据库访问一般是个必备的环节。数据库用来存储业务数据,供应用程序访问。在 Java 技术的应用体系中,应用程序是通过 JDBC(Java Database Connectivity)实现的接口来访问数据库的, JDBC 支持“建立连接、SQL 语句查询、处理结果”等基本功能。在应用 JDBC 接口访问数据库的过程中,只要根据规范来实现,就可以达到要求的功能。但是,有些时候进行数据查询的效率着实让开发人员不如所愿,明明根据规范编写的程序,运行效果却很差,造成整个系统的执行效率不高。使用速度快的 JDBC 驱动JDBC API 包括两种实现接口形式,一种是纯 Java 实现的驱动,一
21、种利用 ODBC 驱动和数据库客户端实现,具体有四种驱动模式并各有不同的应用范围,针对不同的应用开发要选择合适的 JDBC 驱动,在同一个应用系统中,如果选择不同的 JDBC 驱动,在效率上会有差别。例如,有一个企业应用系统,不要求支持不同厂商的数据库,这时就可以选择模式 4的 JDBC 驱动,该驱动一般由数据库厂商实现的基于本地协议的驱动,直接调用数据库管理系统使用的协议,减少了模式 3 中的中间层。使用 JDBC 连接池为了提高访问数据库的性能,我们还可以使用 JDBC 2.0 的一些规范和特性,JDBC是占用资源的,在使用数据库连接时可以使用连接池 Connection Pooling,
22、避免频繁打开、关闭 Connection。而我们知道,获取 Connection 是比较消耗系统资源的。Connection 缓冲池是这样工作的:当一个应用程序关闭一个数据库连接时,这个连接并不真正释放而是被循环利用,建立连接是消耗较大的操作,循环利用连接可以显著的提高性能,因为可以减少新连接的建立。一个通过 DataSource 获取缓冲池获得连接,并连接到一个 CustomerDB 数据源的代码演示如下:Context ctx = new InitialContext();DataSource dataSource = (DataSource) ctx.lookup(“jdbc/Custo
23、merDB“);Connection conn = dataSource.getConnection(“password“,“username“);缓存 DataSource一个 DataSource 对象代表一个实际的数据源。这个数据源可以是从关系数据库到表格形式的文件,完全依赖于它是怎样实现的,一个数据源对象注册到 JNDI 名字服务后,应用程序就可以从 JNDI 服务器上取得该对象,并使用之和数据源建立连接。通过上面的例子,我们知道 DataSource 是从连接池获得连接的一种方式,通过JNDI 方式获得,是占用资源的。为了避免再次的 JNDI 调用,可以系统中缓存要使用的 DataS
24、ource。关闭所有使用的资源系统一般是并发的系统,在每次申请和使用完资源后,应该释放供别人使用,数据库资源每个模式的含义可以参考 SUN JDBC 的文档,不同是比较宝贵的,使用完成后应该保证彻底的释放。 请看下面的代码段: Connection conn = null; Statement stmt = null; ResultSet rs = null; try DataSource dataSource = getDataSource(); / 取的 DataSource 的方法,实现略。conn = datasource.getConnection(); stmt = conn.cr
25、eateStatement(); rs = stmt.executeQuery(“SELECT * FROM .“); . / 其他处理rs.close();stmt.close();conn.close();catch (SQLException ex) . / 错误处理粗看似乎没有什么问题,也有关闭相关如 Connection 等系统资源的代码,但当出现异常后,关闭资源的代码可能并不被执行,为保证资源的确实已被关闭,应该把资源关闭的代码放到 finally 块:Connection conn = null; Statement stmt = null; ResultSet rs = nul
26、l; try DataSource dataSource = getDataSource(); / 取的 DataSource 的方法,实现略。conn = datasource.getConnection(); stmt = conn.createStatement(); rs = stmt.executeQuery(“SELECT * FROM .“); . / 其他处理catch (SQLException ex) . / 错误处理finally if (rs!=null) try rs.close(); / 关闭 ResultSet catch (SQLException ex) .
27、/ 错误处理 if (stmt!=null) try stmt.close(); / 关闭 Statement catch (SQLException ex) . / 错误处理 if (conn!=null) try conn.close(); / 关闭 Connection catch (SQLException ex) . / 错误处理 大型数据量处理当我们在读取诸如数据列表、报表等大量数据时,可以发现使用 EJB 的方法是非常慢的,这时可以使用直接访问数据库的方法,用 SQL 直接存取数据,从而消除 EJB 的经常开支(例如远程方法调用、事务管理和数据序列化,对象的构造等)。缓存经常使用
28、的数据对于构建的业务系统,如果有些数据要经常要从数据库中读取,同时,这些数据又不经常变化,这些数据就可以在系统中缓存起来,使用时直接读取缓存,而不用频繁的访问数据库读取数据。缓存工作可以在系统初始化时一次性读取数据,特别是一些只读的数据,当数据更新时更新数据库内容,同时更新缓存的数据值。一个例子是,在一套企业应用系统中,企业的信息数据(如企业的名称)在多个业务应用模块中使用,这时就可以把这些数据缓存起来,需要时直接读取缓存的企业信息数据。七、总结一般意义上说,参与系统运行的代码都会对性能产生影响,实际应用中应该养成良好的编程规范、编写高质量的代码,当系统性能出现问题时,要找到主要影响性能的瓶颈所在,然后集中精力优化这些代码,能达到事半功倍的效果。J2EE 性能的优化包括很多方面的,要达到一个性能优良的系统,除了关注代码之外,还应该根据系统实际的运行情况,从服务器软硬件环境、集群技术、系统构架设计、系统部署环境、数据结构、算法设计等方面综合考虑。