调优案例分析与实战
案例分析
高性能硬件上的程序部署策略
- 通过64位JDK来使用大内存
- 使用若干个32位虚拟机建立逻辑集群来利用硬件资源。
如果读者计划使用64位JDK来管理大内存,还需要考虑下面可能面临的问题:
- 内存回收导致的长时间停顿。
- 现阶段,64位JDK的性能测试结果普遍低于32位JDK。
- 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照,哪怕产生了转储快照也几乎无法进行分析。
- 相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。
这些问题听起来吓人,所以现阶段不少管理员选择第二种方式:使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,分配不同端口,然后在前端搭建一个负载均衡器,以及反向代理的方式来分配访问请求。
当然做集群部署,可能会遇到那面的一些问题:
- 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争。
- 很难最高效地利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点任有较多空余,尽管可以使用JNDI,但是这个有一定的复杂性,并带来额外的性能开销。
- 各个节点任然不可避免地受到32位的内存限制,在32位Windows平台中,每个进程只能使用2GB的内存,考虑到内存开销,堆一般给1.5G.在Linux或Unix系统中,可以提升到3G乃至接近4G内存。
- 大量使用本地缓存(如大量使用HashMap作为K/V缓存)的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点上都有一份缓存,这时候可以考虑把本地缓存改为集中式缓存。
集群间同步导致的内存溢出
在服务使用过程中,往往一个页面会产生数次乃至数十次的请求,因此这个过滤器导致集群各个节点之间网络交互非常频繁。当网络情况不能满足传输要求时,重发数据在内存中不断堆积,很快就产生了内存溢出。
堆外内存导致的溢出错误
例如一个学校的小型项目:基于B/S的电子考试系统,为了实现客户端能实时地从服务器端接收考试数据,系统使用了逆向AJAX技术(也称为Comet或者Server Side Push),选用CometD1.1.1作为服务器端推送框架,服务器是Jetty7.1.4,硬件为一台普通PC机,Core i5CPU, 4GB内存,运行32位Windows操作系统。
没有考虑到堆外内存占用比较高,32位windows平台对每个进程内存限制2G,程序中使用到了CometD 1.1.1框架,有大量的NIO操作需要用到Direct Memory内存,Direct Memory分配不足导致的内存溢出。
从实践经验的角度出发,除了 Java 堆和永久代之外,我们注意到下面这些区域还会占用较多的内存:
- Direct Memory: 可通过- XX: MaxDirectMemorySize 调整大小(HotSpot VM无此参数:http://rednaxelafx.iteye.com/blog/1098791,http://www.dongliu.net/post/504141),内存不足时抛出 OutOfMemoryError 或者 OutOfMemoryError: Direct buffer memory。
- 线程堆栈:可通过-Xss 调整大小,内存不足时抛出 StackOverflowError( 纵向无法分配,即无法分配新的栈帧)或者 OutOfMemoryError: unable to create >>li:new native thread( 横向无法分配,即无法建立新的线程)。
- Socket 缓存区:每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出 IOException: Too many open files 异常。
- JNI 代码:如果代码中使用 JNI 调用本地库,那本地库使用的内存也不在堆中。
- 虚拟机和 GC: 虚拟机、 GC 的代码执行也要消耗一定的内存。
外部命令导致系统缓慢
大并发的时候,通过mpstat工具发现CPU使用率很高。
通过Solaris 10的Dtrace脚本可以查看当前情况下那些系统调用话费最多的CPU资源。
结果是fork,用来产生新进程的,Java中不应该有新的进程的产生。
最终找到了答案:每个用户请求的处理都需要执行一个外部 shell 脚本来获得系统的一些信息。执行这个 shell 脚本是通过 Java 的 Runtime. getRuntime(). exec() 方法来调用的。
Java 虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再 退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是 CPU, 内存负担也很重。
服务器JVM进程崩溃
跨系统集成的时候,使用到异步方式调用Web服务,由于两边服务速度不读等,导致很多Web服务没有调用完成,在等待的线程和Socket连接越来越多,超过JVM的承受范围后JVM进程就崩溃了。
可以将异步调用改为生产者/消费者模式的消息队列实现。
测试工具:SoapUI
不恰当数据结构导致内存占用过大
垃圾收集器: ParNew + CMS
在内存中存入了100万个HashMap,就会GC造成停顿。
ParNew 收集器使用的是复制算法,这个算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如果存活对象过多,把这些对象复制到 Survivor 并维持这些对象引用的正确就成为一个沉重的负担,因此导致 GC 暂停时间明显变长。
如果不修改程序,仅从 GC 调优的角度去解决这个问题,可以考虑将 Survivor 空间去掉(加入参数- XX: SurvivorRatio= 65536、- XX: MaxTenuringThreshold= 0 或者- XX:+ AlwaysTenure), 让新生代中存活的对象在第一次 Minor GC 后立即进入老年代,等到 Major GC 的时候再清理它们。这种措施可以治标,但也有很大副作用,治本的方案需要修改程序,因为这里的问题产生的根本原因是用 HashMap < Long, Long >结构来存储数据文件空间效率太低。
HashMap
由Windows虚拟内存导致的长时间停顿
程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了,这样发生 GC 时就有可能因为恢复页面文件的操作而导致不正常的 GC 停顿。
在 Java 的 GUI 程序中要避免这种现象,可以加入参数"- Dsun. awt.keepWorkingSetOnMinimize= true" 来解决。
实战:Eclipse运行速度调优
这部分知识暂时不看,有时间再来回顾