您的位置:首页 > 编程语言 > Java开发

深入学习Java JVM - 调优案例分析与实战

2015-09-13 00:00 645 查看
摘要: 学习笔记

对于用户交互性强,对停顿时间敏感的系统,可以给Java虚拟机分配超大堆的前提是有把握把应用程序的Full GC(老年代垃圾收集器回收垃圾)频率控制得足够低,至少要低到不会影响用户使用,比如 : 十几个小时乃至一天才出现一次Full GC,这样可以通过在深夜执行定时任务的方式出发Full GC甚至自动重启服务器来保持内存可用空间在一个稳定的水平.

控制Full GC 频率的关键是看应用的绝大多数对象是否符合 “朝生夕灭” 的原则,即大对数对象的生存时间不应太长,尤其是不能有成批量的,长生存时间的大对象产生,这样才能保证老年代空间的稳定.

在大多数网站形式的应用里,主要对象的生存周期都应该是请求级或页面级的,回话级和全局级的长生命对象相对减少。只要代码写得合理,应当都能实现在超大堆中正常使用而没有Full GC,这样的话,使用超大堆内存时,网站响应的速度才比较有保证。除此之外,如果读者计划使用64位JDK来管理大内存,还需要考虑下面可能面临的问题

1. 内存回收导致的长时间停顿

2. 现阶段,64位JDK的性能测试结果普遍低于32位JDK。
3. 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎无法产生堆转储快照(因为要产生十几GB乃至更大的dump文件),哪怕产生了快照也几乎无法进行分析。
4. 相同的程序在64位JDK中消耗的内存一般比32位JDK大,这是由于指针膨胀及数据型对齐补白等因素导致的。
上面的问题听起来有点吓人,所以现价段不少管理员还是选择了第二种方式:使用若干个32位虚拟建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,给每个服务器进行分配不同的端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。读者不需要太在意均衡器转发所消耗的性能,即使使用64位JDK,许多应用也不止有一台服务器,因此许多应用中前段的均衡器总是要存在的。

考虑到一台物理机器上建立逻辑集群的目的仅仅是尽可能地利用硬件资源,并不需要关心状态保留、热转移之类的高可用性需求,也不需要保证每个虚拟机进程有绝对准确的均衡负载,因此使用无Session复制的亲合式集群是一个相当不错的选择。我们仅仅需呀保障集群具备亲和性,也就是均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可,这样程序开发阶段就基本不用为集群环境做什么特别的考虑。

当然,很少有没有缺点的方案,如果读者计划使用逻辑集群的方式来部署程序,可能会遇到下面的一些问题。

1. 尽量避免节点竞争全局资源,最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件的话(尤其是并发写操作容易出现问题),很容易导致IO异常。

2. 很难最高效率地利用某些资源池,如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点仍有较多空余。尽管可以使用集中式的JNDI,但这有一定的复杂性且可能带来额外的性能代价。

3. 各个节点仍然不可避免地受到32位的内存限制,在32位Windows平台中每个进程只能使用2GB的内存,考虑到堆以外的内存开销,堆一般最多只能开到1.5GB。在某些Linux,Unix系统(如Solaris)中,可以提升到3GB乃至接近4GB的内存,但32位

中仍然受最高4GB(2^32)内存的限制。

4. 大量使用本地缓存(如大量使用HashMap所谓K/V缓存)的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点上都有一份缓存,这时可以考虑把本地缓存改成集中式缓存

介绍完这两种部署方式,再重新回到这个案例中,最后的部署方案调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB的内存。另外建立一个Apache服务作为前端均衡器代理访问门户。考虑到用户对响应较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比硬件升级前有较多提升。



堆外内存导致的溢出错误

环境:基于B\S的点子考-试系统,为了发现客户端能实时地从服务端接收考-试数据,系统使用了逆向AJAX技术(也称Comet或Server Side Push),选用CometD1.1.1作为服务端推送框架,服务器是Jetty7.1.4,硬件为一台普通PC机,Core i5 CPU,
4G内存,运行32位Windows操作系统。

说明:测试期间发现服务端不定时抛出内存溢出异常,服务器不一定每次都会出现异常,但是假如正式考-试时奔溃一次,那估计整场考-试都会全乱套,网站管理员尝试过把堆开到最大,32位系统最多到1.6GB基本无法再加大了,而且开大量也基本没效果,抛出
内存溢出异常好像更加繁琐了。加入-XX:+HeapDumpOnOutOfMemoryError,居然也没有任何反应,抛出内存溢出异常时什么文件都没产生。无奈之下只好挂着jstat使劲盯屏幕,发现GC并不频繁,Eden区,Survivor区,老年代及拥挤代内存全部

表示"情绪稳定,压力不大",但是照样不停的抛出内存溢出异常,管理员鸭梨很大。最后,在内存溢出后从系统日志中找到异常堆栈。

分析:大家都知道操作系统对每个进程能管理的内存是有限的,这台服务器使用的32位Windows平台的限制是2GB,其中给了Java堆1.6GB,而Direct Memory 并不算在1.6GB的堆之内,因此它只能在剩余的0.4GB空间分出一部分。在此应用中导致内

存溢出的关键是:垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory 却不能像新生代和老年代那样,发现空间不足了就通知收集器进行垃圾回收,他只能等到抛出内存溢出异常时,先catch掉,再在catch块里面“大喊”

“System.gc”.要是虚拟机还是不听(如:打开了-XX:+DisableExplicitGC开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存异常了。而本案例中使用的Comet1.1.1框架,正好有大量的NIO操作需要用到Direct Memory。

总结:从实践经验来看,除了java堆和永久代之外,我们注意到下面这些区域也会占用较多的内存,这里所有的内存总和会受到操作系统进程最大内存的限制:

1. Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct buffer memory。

2. 线程堆栈:可通过-Xss调整大小内存不足时抛出StackoverflowErroe(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。

3. Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB的内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。

4. JNI代码:如果代码中使用JNI调用本地库,那么本地库使用内存也不在堆中

5. 虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。

服务器JVM进程奔溃

环境:一个基于B/S的MIS系统,硬件为2个CPU、8GB内存的HP系统,服务器是WebLogic9.2(就是第二个案例中的那个系统)。正常运行一段时间后,最近发现在运行期间频繁出现集群节点的虚拟机进程自动关闭的现象,留下一个hs_err_pid###.log文件后,进程就消失了,两台物理机里的每个节点都出现过进程奔溃的现象。从系统日志中注意到,每个节点的虚拟机进程在奔溃前不久,都发生过大量相同的异常。
Java.net.SocketException : Connection reset
分析:这是一个远端断开连接的异常,通过系统管理员了解到系统最后一个OA门户做了集成,在MIS系统工作流的待办事项变化时,要通过Web服务通知OA门户系统,把待办事项的变化同步到OA门户之中。通过SoapUI测试了一下同步待办事项的几个Web
服务,发现调用后竟然需要长达3分钟才能返回,并且返回的结果都是连接中断。

由于MIS系统的用户多,待办事项变化很快,为了不被OA系统的速度拖累,使用了异步的方式调用Web服务,但由于两边服务的速度完全不对等,时间越长就积累了越多Web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终超过虚拟

机的承受能力后使得虚拟机进程奔溃。通知OA门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正常
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: