调优案例分析与实战

案例分析

高性能硬件上的程序部署策略

  1. 通过64位JDK来使用大内存
  2. 使用若干个32位虚拟机建立逻辑集群来利用硬件资源。

如果读者计划使用64位JDK来管理大内存,还需要考虑下面可能面临的问题:

  • 内存回收导致的长时间停顿。
  • 现阶段,64位JDK的性能测试结果普遍低于32位JDK。
  • 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照,哪怕产生了转储快照也几乎无法进行分析。
  • 相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。

这些问题听起来吓人,所以现阶段不少管理员选择第二种方式:使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,分配不同端口,然后在前端搭建一个负载均衡器,以及反向代理的方式来分配访问请求。

当然做集群部署,可能会遇到那面的一些问题:

  1. 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争。
  2. 很难最高效地利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点任有较多空余,尽管可以使用JNDI,但是这个有一定的复杂性,并带来额外的性能开销。
  3. 各个节点任然不可避免地受到32位的内存限制,在32位Windows平台中,每个进程只能使用2GB的内存,考虑到内存开销,堆一般给1.5G.在Linux或Unix系统中,可以提升到3G乃至接近4G内存。
  4. 大量使用本地缓存(如大量使用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分别具有 8B 的 MarkWord、 8B 的 Klass 指针,在加 8B 存储数据的 long 值。在这两个 Long 对象组成 Map. Entry 之后,又多了 16B 的对象头,然后一个 8B 的 next 字段和 4B 的 int 型的 hash 字段,为了对齐,还必须添加 4B 的空白填充,最后还有HashMap 中对这个 Entry 的 8B 的引用,这样增加两个长整型数字,实际耗费的内存为( Long( 24B) × 2)+ Entry( 32B)+ HashMap Ref( 8B)= 88B, 空间效率为 16B/ 88B= 18%, 实在太低了。

由Windows虚拟内存导致的长时间停顿

程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了,这样发生 GC 时就有可能因为恢复页面文件的操作而导致不正常的 GC 停顿。

在 Java 的 GUI 程序中要避免这种现象,可以加入参数"- Dsun. awt.keepWorkingSetOnMinimize= true" 来解决。

实战:Eclipse运行速度调优

这部分知识暂时不看,有时间再来回顾

参考

JVM笔记 – 自动内存管理机制(调优案例分析与实战)

笔记:深入理解JVM 第5章 调优案例分析与实战

results matching ""

    No results matching ""