下面讨论的J2EE问题适合于使用JSP(Java Server Pages)、EJB(Enterprise JavaBean)或JDBC(Java Data Base Connectivity,java数据库连接)的应用。
一、使用缓冲标记
一些应用服务器加入了面向JSP的缓冲标记功能。例如,BEA的WebLogic Server从6.0版本开始支持这个功能,Open Symphony工程也同样支持这个功能。JSP缓冲标记既能够缓冲页面片断,也能够缓冲整个页面。当JSP页面执行时,如果目标片断已经在缓冲之中,则 生成该片断的代码就不用再执行。页面级缓冲捕获对指定URL的请求,并缓冲整个结果页面。对于购物篮、目录以及门户网站的主页来说,这个功能极其有用。对于这类应用,页面级缓冲能够保存页面执行的结果,供后继请求使用。
对于代码逻辑复杂的页面,利用缓冲标记提高性能的效果比较明显;反之,效果可能略逊一筹。
二、始终通过会话Bean访问实体Bean
直接访问实体Bean不利于性能。当客户程序远程访问实体Bean时,每一个get方法都是一个远程调用。访问实体Bean的会话Bean是本地的, 能够把所有数据组织成一个结构,然后返回它的值。
用会话Bean封装对实体Bean的访问能够改进事务管理,因为会话Bean只有在到达事务边界时才会提交。每一个对get方法的直接调用产生一个事务,容器将在每一个实体Bean的事务之后执行一个“装入-读取”操作。
一些时候,使用实体Bean会导致程序性能不佳。如果实体Bean的唯一用途就是提取和更新数据,改成在会话Bean之内利用JDBC访问数据库可以得到更好的性能。
三、选择合适的引用机制
在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种: include指令,以及include动作。
include指令:例如该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。
include动作:例如该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请 求没有出现之前,被引用的页面无法确定时,使用include动作才合算。
四、在部署描述器中设置只读属性
实体Bean的部署描述器允许把所有get方法设置成“只读”。当某个事务单元的工作只包含执行读取操作的方法时,设置只读属性有利于提高性能,因为容器不必再执行存储操作。
五、缓冲对EJB Home的访问
EJB Home接口通过JNDI名称查找获得。这个操作需要相当可观的开销。JNDI查找最好放入Servlet的init()方法里面。如果应用中多处频繁地 出现EJB访问,最好创建一个EJBHomeCache类。EJBHomeCache类一般应该作为singleton实现。
六、为EJB实现本地接口
本地接口是EJB 2.0规范新增的内容,它使得Bean能够避免远程调用的开销。所有数据和返回值都通过引用的方式传递,而不是传递值。本地接口必须在EJB部署的机器上使用。简而言之,客户程序和提供服务的组件必须在同一个JVM上运行。如果Bean实现了本地接口,则其引用不可串行化。
七、生成主键
在EJB之内生成主键有许多途径,下面分析了几种常见的办法以及它们的特点。
利用数据库内建的标识机制(SQL Server的IDENTITY或Oracle的SEQUENCE)。这种方法的缺点是EJB可移植性差。
由实体Bean自己计算主键值(比如做增量操作)。它的缺点是要求事务可串行化,而且速度也较慢。
利用NTP之类的时钟服务。这要求有面向特定平台的本地代码,从而把Bean固定到了特定的OS之上。另外,它还导致了这样一种可能,即在多CPU的 服务器上,同一个毫秒之内生成了两个主键。
借鉴Microsoft的思路,在Bean中创建一个GUID。然而,如果不求助于JNI,Java不能确定网卡的MAC地址;如果使用JNI,则程序就要依赖于特定的OS。
还有其他几种办法,但这些办法同样都有各自的局限。似乎只有一个答案比较理想:结合运用RMI和JNDI。先通过RMI注册把RMI远程对象绑定到 JNDI树。客户程序通过JNDI进行查找。
八、及时清除不再需要的会话
许多通常的Java性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者开始考虑性能问题之前. 在这个系列中, Brian Goetz 讨论了通常的 Java 性能上的冒险以及怎么在设计时候避免它们. 在第二部分, 他讨论了减少临时对象创建的一些技术。
虽然许多程序员把性能管理一直推迟到开发过程的最后, 性能考虑应该从第一天起就和设计周期结合在一起. 这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法.所以下面继续探索大量临时对象创建的问题, 并且提供一些避免它们的技术.
临时对象就是一些生命周期比较短的对象, 一般用于保存其他数据而再没有其他用途. 程序员一般用临时变量向一个方法传递数据或者从一个方法返回数据. 第一部分探讨了临时对象是怎样给一个程序的性能带来负面的冲击, 以及一些类接口的设计思想怎样提供了临时对象的创建. 避免了那些接口的创建, 你就能极大地减少那些影响你的程序性能的临时对象创建的需求。
那么是不是只是对 String 说不。其实当它要创建临时变量时, String 类是最大的罪人之一.
大量使用 BadREgExpMatcher 的程序比使用 BtterRegExpMatcher 的要慢好多. 首先,调用者不得不创建一个 String 传入 match(), 接着 match() 又创建了一个 String 来返回匹配的文本. 结果是每次调用都有两个对象创建, 看起来不多, 但是如果要经常调用match(), 这些对象创建带给性能的代价就太大了.
BadRegExpMatcher 的性能问题不是在它的实现中, 而是它的接口; 象它定义的接口, 没有办法避免一些临时变量的创建。
BetterRegExpMatcher 的 match() 用原类型(整数和字符数组)代替了 String 对象; 不需要创建中间对象来在调用者和 match() 之间传递信息.
既然在设计时候避免性能问题要比写完整个系统以后再修改要容易一些, 你应该注意你的类中控制对象创建的方法. 在 RegExpMatcher 的例子中, 它的方法要求和返回 String 对象, 就应该为潜在的性能冒险提个警告信号. 因为 String 类是不可变的, 除了最常用以外, 所有的 String 参数在每次调用处理函数时都需要创建一个新的 String.
那么不可变性对于性能有很大影响。因为 String 经常和大量的对象创建联系在一起, 一般来说归咎于它的不可变性. 许多程序员认为不可变的对象与生俱来对性能没有好处. 但是, 事实多少会更复杂一些. 实际上, 不可变性有时候提供了性能上的优势, 可变性的对象有时候导致性能问题. 不管可变性对性能来说有帮助或者有害, 依赖于对象是怎么使用的.
程序经常处理和修改文本字符串 -- 这和不可变性非常不匹配。每次你想处理一个 String --想查找和解析出前缀或者子串, 变小写或者大写, 或者把两个字符串合并 -- 你必须创建一个新的 String 对象. (在合并的情况下, 编译器也会隐藏地创建一个 StringBuffer() 对象)
另一个方面, 一个不可变的对象的一个引用可以自由共享, 而不用担心被引用的对象要被修改, 这个比可变对象提供性能优势。
可变的对象有它们自己的临时对象问题.
在 RegExpMatcher 的例子中, 你看见了 当一个方法返回一个 String 类型时, 它通常需要新建一个 String 对象. BadRegExpMatcher 的一个问题就是 match() 返回一个对象而不是一个原类型 -- 但是只因为一个方法返回一个对象, 不意味着必须有一个新对象创建. 考虑一下 java.awt 中的几何类, 象 Point 和 Rectangle. 一个 Rectangle只是四个整数(x, y, 宽度, 长度)的容器, AWT Component 类存储组件的位置, 通过getBounds()作为一个Rectangle 返回,getBounds() 只是一个存储元 -- 它只使一些 Component 内部的一些状态信息可用. getBounds() 需要创建它返回的 Rectangle 吗? getBounds(), 没有新对象创建 -- 因为组件已经知道它在哪里 -- 所以 getBounds() 效率很高. 但是 Rectangle 的可变性又有了其他问题. 当一个调用者运行一下程序会发生什么呢?Rectangle 是可变的, 它在 Component 不知道的情况下使 Component 移动. 对象AWT 这样的 GUI 工具箱来说, 这是个灾难, 因为当一个组件移动以后, 屏幕需要重绘, 组件监听器需要被通知, 等等. 所以上面的实现 Component.getBounds() 的代码看起来很危险. 但是现在, 每一个 getBounds() 的调用都创建一个新对象, 就象 RegExpMatcher 一样.在 String 的情况中, 对象创建是必要的, 因为 String 是不可变的. 对象的创建也是必要的, 因为 Rectangle 是可变的. 我们使用 String 避免了这个问题,在我们的接口中没有使用对象. 虽然在 RegExpMatcher 的情况下很好, 这个方法不总是可行的或者是希望的. 幸运的是, 你可以在实际类的时候可以使用一些技巧, 来免除太多小对象的问题, 而不是完全避免小对象.
减少对象的技巧 1: 加上好的存取函数
在 Swing 工具箱的初始版本中, 小对象的临时创建, 象 Point, Rectangle 和 Dimension极大地阻碍了性能. 把它们放在一个 Point 或者 Rectangle 中来一次返回多个值, 看起来更有效, 实际上, 对象的创建比多个方法调用代价更高. 在 Swing 的最后发布之前, 通过给 Component 和其他一些类加一些新的存取方法, 问题就简单地解决了。getBounds() 的旧形式仍然支持; 好的存取方法简单地提供了有效的方法来达到相同的目的. 结果是, Rectangle 的接口全部在 Component 中使用. 当修改 Swing 包支持和使用这样的存取函数后, 在许多 Swing 操作中比以前要快到两倍. 这很好, 因为 GUI 代码非常注意性能 -- 用户等待发生一些事, 希望 UI 操作瞬间完成.
使用这个技术不好的地方就是你的对象提供了更多的方法, 有多于一个的方法来得到相同的信息, 就使文档更大更复杂, 可能使用户害怕. 但是就象 Swing 的例子显示的, 在关注性能的情况下, 这样的优化技术是有效的.
技巧 2: 利用可变性
除了给 Component 加上原类型的存储函数 -- 象上面讨论的 getX() 函数 -- 以外, Java 2 在 AWT 和 Swing 中也使用了另一种技术来减少对象创建, 允许一个调用者把边界作为一个 Rectangle 得到, 但是不需要任何临时对象的创建.调用者仍然需要创建一个 Rectangle 对象, 但它可以在后来的调用中重用. 如果一个调用者在一系列的 Component 中循环, 可以只创建一个 Rectangle 对象, 在每个 Component 中重用. 注意这个技术只用于可变性对象; 你不能用这种方法消除 String 的创建.
技巧 3: 得到两个中的最好的.
一个解决在简单类(象 Point 之类)的对象创建的问题, 更好的方法是使 Point 对象不可变,但是定义一个可变的子类。 Shape 可以安全返回一个 myLocation 的引用, 因为调用者试图修改域或者调用设置函数会失败. (当然, 调用者仍然可以把 Point 转换为 MutablePoint, 但这明显不安全, 这样的调用者可能得到他们想要的)。
这个技巧 -- 返回一个具有可变的和不可变的类, 只允许读对象, 而不创建新对象 --在 Java 1.3 类库 java.math.BigInteger 类中使用. MutableBigInteger 类不可见 --它是一个只在 java.math 类库中内部使用的私有类型. 但是既然 BigInteger 的一些方法(象 gcd()) 在许多数学操作中都有, 在一个地方操作比创建上百个临时变量性能提高非常大.
总结
一、所有的网站性能优化之中, 值得记住的是有许多程序的性能可以完全接受的情况. 在这些情况下, 不值得牺牲可读性, 可维护性, 抽象, 或者其他可取的程序属性来获得性能. 但是, 既然许多性能问题的种子在设计时就种下了, 要注意到设计思想潜在地对性能的冲击,当你设计的类在关注性能的情况使用, 你可以有效地使用这里提到的技巧来减少临时对象的创建
二、Tomcat性能优化可从外部环境和自身调整两方面着手。外部环境主要是Tomcat所在服务器的运行环境,包括操作系统层面、部署以及Java虚拟机的配置。
三、操作系统。这里不再赘述,跟优化其他服务器的思路与步骤没有本质区别。尽可能的增大可使用的内存容量、提高CPU的频率、保证文件系统的读写速率等等。对于可能发生很大并发连接的情况,可能需要修改内核参数来设置最大连接数。
四、Java虚拟机。Sun的JVM应该是多数情况下的第一选择。在满足项目要求的前提下可以选用版本较高的JVM版本,一般来说高版本产品在速度和效率上比低版本会有改进。由于Jvm系统垃圾收集机制的存在,在高负载情况下如果能根据系统的具体要求有效的调整最优化堆的大小,也可以起到一定优化作用。如果堆设置较大,则GC次数变少,但每次花费较长时间,从而导致系统处理能力抖动较大;如果堆设置较小,则GC变得频繁,虽然对系统性能影响较小,但频繁的GC也会耗费系统资源。
五、JVM动态库有Server和Client两个版本,虽然差别不是很大,但生产环境还是推荐使用Server版本。除此之外,主要的JVM还包括BEA JRockit,IBM JVM,Jikes RVM,Kaffe等。可以根据项目的中间件产品选择对应厂商的JVM来获取有针对性的性能优化。
六、Tomcat自身的调整策略
"启动参数 "
Tomcat默认可以使用的内存是128MB。可以通过在启动时加入“-Xms”和“-Xmx”参数来获得更大的内存分配。但也要注意GC的问题。
"负载均衡 "
比较复杂,有机会另文详述。
"集成Web服务器处理静态内容 "
作 为一个Jsp/Servlet容器,Tomcat本身对静态Html文件的相应速度远逊Apache等Web服务器。通过与此类Web服务器的集成,可以 将对jsp内容的请求转发至Tomcat,而用Web服务器处理静态内容,能够非常显著的降低整体负载,提高整体响应的速度。
"调整线程数 "
Tomcat5使用线程池(Apache Portable Runtime)来加速响应速度。默认创建5个线程,最大 线程数是200.如果并发较大,则可以对以下几个参数进行具体的调整:
maxThreads:Tomcat可创建的最大线程数;
acceptCount:如果当前可用线程数为0,则将请求放入处理队列中。这个值限定了请求队列的大小,超过这个数值的请求将不予处理。
connectionTimeout:网络连接超时数,单位毫秒。
minSpareThreads:如果当前没有空闲线程,且没有超过maxThreads,一次性创建的空闲线程数量。Tomcat初始化时创建的线程数量也由此值设置。
maxSpareThreads:一旦创建的线程超过此数值,Tomcat会关闭不再需要的线程。
线程数可以大致上用 “同时在线人数*每秒用户操作次数*系统平均操作时间” 来计算。
"使用Jikes "
Jikes是性能优良的Java编译器,在jsp容器中使用Jikes替代Sun的编译器能够有效提高jsp文件的编译速度。对于Tomcat 5.5,可以参考Jasper-Howto。要注意的是Windows版本的Jikes是不支持-encoding选项的,你需要自己编译。可以参考这里。
"用Ant进行预编译 "
Tomcat官方推荐的部署方式是使用Ant对jsp进行预编译。直接在部署时将jsp编译为servlet类,能够在运行初期时显著提高响应速度。Web2.0的兴起,掀起了互联网新一轮的网络创业大潮。以用户为导向的新网站建设概念,细分了网站功能和用户群,不仅成功的造就了一大批新生的网站,也极大的方便了上网的人们。但Web2.0以用户为导向的理念,使得新生的网站有了新的特点--高并发,高流量,数据量大,逻辑复杂等,对网站建设也提出了新的要求。