前面6篇Blog我们完成了最初的JVM内存区域,对象的回收与分配的理论加常用工具(jvisualvm无敌!)的学习,今天我们来实战一下!
案例1 高性能硬件上的程序部署策略
一个15万pv((Page View)/天的在线文档类的网站,最近更换了硬件系统,新的硬件系统为4个CPU,16G物理内存,64位CentOS 5.4,Resin作为WEB服务器,没有别的应用.管理员为了尽量利用资源,选择64位的JDK,
-Xms
与-Xms
将heap固定为12G禁止扩容,问题:经常不定期产生长时间失去响应
监控服务器进行情况后,如使用jstat -gcutil 1748
来看各阶段GC时间,总GC时间,使用了PS(复制)+PSOLD(标记整理)收集器,回收12GB的堆,一次FGC停顿高达14s
由于程序的原因,访问文档时要讲文档序列化进入内存,产生大对象,大对象在YGC中复制的时候,由于Survivor放不下,启动老年代的担保直接进入老年代,随着老年代容量的填满引发FGC,引发停顿
即核心问题是:过大的堆内存进行回收时,带来的长时间停顿
在高性能硬件上部署程序,主要有2种方式
- 通过64位JDK使用大内存
- 通过若干个32位JVM建立逻辑集群,如nginx反向代理
本案例中,使用了第一种方式,但是分配超大堆的前提是能够控制好FGC频率足够低,而控制FGC频率的关键就是绝大多数对象符合朝生夕灭,尤其是不能有成批量的,长时间生存的大对象,这样才能保证老年代空间的稳定
在大多数网站应用里,主要的对象都是请求级的或者页面级的,会话级和全局级的长生命对象很少,按照这个原则,才能在超大堆中正常使用没有FGC,网站速度才能有保证
64位JDK的问题
- 内存回收导致的STW
- 64位性能普遍低于32位
- 需要保持足够稳定,dump巨大无法分析
- 相同程序64位消耗的内存比32位大
所以说现阶段不少管理员采用若干32位虚拟机建立逻辑集群来利用硬件资源,如使用nginx反向代理分配请求
逻辑集群的问题
- 节点竞争全局资源,如竞争磁盘IO
- 很难高效利用某些资源池,如数据库连接池
- 受到32位内存限制,4GB(2^32)
- 使用本地缓存的话,在每个节点都要有缓存,造成内存浪费,可以使用集中式缓存
最终解决方案
由于本网站主要是文档门户,cpu不敏感,所以没必要用PS+PSOld吞吐量组合,换成ParNew+CMS,降低STW,部署则调整为建立5个32位JDK逻辑集群,每个进程2GB内存,heap固定1.5G,效果改善明显
从这个案例我们学到了
1.64位JVM虽然可以使用更大的heap,但是对超大堆的GC就要更久,我们要把握住程序中大对象的生命周期,避免长期占用老年代,引发FGC导致停顿 2.IO密集型不关注吞吐量,关注反应时间,所以使用ParNew+CMS
案例二 集群间同步导致内存溢出
案例五 服务器JVM进程崩溃(这两个环境一样,一起说)
有一个基于B/S的MIS(管理信息系统(Management Information System)系统,硬件为两台,2个CPU,8G内存的HP小型机,服务器weblogic9.2,每台启动了3个weblogic实例,构成一个6个节点的亲和式集群,节点之间没有session同步,但是有一些数据需要同步,开始这些数据放在数据库中,由于读写频繁竞争激烈,所以使用JBOSSCaChe构建了一个全局缓存,全局缓存启用后,服务正常了一段较长的时间,但是最近不定期的出现了多次内存溢出的问题
有一天它又坏了,出现了JVM进程自动关闭的现象 异常堆栈信息为:java.net.SocketException:Connection reset 这时一个远端断开连接的异常,由于系统中使用Socket做了通知服务,而对方的反应极慢(或者直接连接中断),这边速度很快,所以使用了异步的方式去调用,时间越长就积累了越多Web调用没有完成,等待线程和socket连接越来越多,最终超过JVM承受能力使得JVM崩溃 解决办法 通知对方修复,且将异步调用改为生产者,消费者模式的消息队列后恢复正常 学到了 Socket连接积累过多导致JVM崩溃
session粘性 这种方式也成为亲和式集群,给session创造粘性,意思是让用户每次都访问的同一个应用服务器 这样就要在前端服务器apache中记录下,用户首次访问的是哪个tomcat,将用户后面发送的请求都发送到这个tomcat上去 这样带来的后果是,各个服务器负载不均衡,因为只在用户首次访问的时候,采用了负载均衡分发,但是这个影响也不会那么明显
处理思路
由于是OOM导致的宕机,那么我们需要抓获OOM当时的情况,在进行分析 -XX:+HeapDumpOnOutOfMemoryError,导出当OOM发生时,heap的情况
问题
问题出在使用集群共享的数据,可以允许读操作频繁,因为数据在本地内存有副本,但是不应该有频繁的写操作,会带来很大的网络同步开销,当网络受阻,由于JBOSS框架的原因,传输失败后,发送的信息会在内存中保留,导致大量内存占用,引发OOM
学到了
OOM出现时的处理思路,即输出发生时的dump,分析大量占用内存的对象
案例三 堆外内存导致的溢出错误
一个学校的小型项目:基于B/S的电子考试系统,硬件为普通PC,测试期间发现不定期抛出内存溢出异常,尝试把heap调大没有效果,加入-XX:+HeapDumpOnOutOfMemory没反应,使用jvisualVM,发现gc不频繁,各区无压力 最后从系统日志中找到异常堆栈
//标志性的堆外使用---NIO
...Unsafe.allocateMemory(Native method)//标志性的堆外溢出
解决思路
首先肯定是打印dump分析,查看jvisualVM,都看不出来就要去日志等地方找错误的堆栈,这里就找到了是堆外内存溢出,回忆下堆外内存,它不分配在堆中,默认与堆同样大小,这里进程2G,堆拉到了1.6G,那堆外就去0.4G里分一点
Direct Memory的内存回收:GC时,虚拟机虽然会对直接内存回收,但是不是像新生代老年代那样,空间不足了就回收,而是等老年代满了,FGC时,顺便收一下直接内存
OOM为什么有时候不打印堆栈信息 强制打印使用参数 -XX:-OmitStackTraceInFastThrow 不打印的时候,是JVM进行了优化 优化是,当第一次发生异常(通常为NullPointerException)时,将打印完整的堆栈跟踪,并且JVM会记住堆栈跟踪(或者可能只是代码的位置)。当该异常经常发生时,将不再打印堆栈跟踪,这既可以实现更好的性能,又不会使相同的堆栈跟踪充满日志。
为什么是堆栈信息? 堆栈跟踪(stack trace)是一个方法调用过程的列表,发生异常的时候e.printStackTrace(); 是把产生该异常的一系列方法的调用过程打印出来(最后调用的方法最先打出来),按照方法的调用过程查找异常出现的原因会很方便。 举个小例子,main()方法中按顺序调用了a()方法和b()方法,其中b()方法中又调用了c()方法,c()方法中产生了一个异常,打印后的结果类似 c():产生异常的行号 b():c方法调用的行号 main():b方法调用的行号 结合之前的知识,堆栈是方法的内存模型,执行方法时push,完毕pop
除了Java堆和MetaSpace,下面的区域也会占用较多内存
- Direct Memory :可以通过 -XX:MaxDirectMemorySize调整大小,未设置的话默认是最大heap大小,内存不足时抛出OOM,或者OOM:Direct buffer memory
- 线程堆栈 -Xss,内存不足时抛出StackOverFlowError或者OOM:unable to create new native thread
- Socket缓冲区:每个Socket连接都Receive和Send两个缓冲区,分别占用37KB和25KB内存,连接多的话这一块内存占用也比较可观,无法分配,抛出IOException:Too many open files
- JNI代码 使用Java native interface去调用native方法,native方法使用的内存也不再heap中
- 虚拟机和GC:消耗一定内存
学到了
OOM不仅仅针对heap,寻找各方面的堆栈信息,然后总和考虑各个占用内存的因素,解决OOM
案例四 外部命令导致系统缓慢
一个数字校园应用系统,运行在一台4个CPU的Solaris 10系统上,中间件为GlassFish服务器,做大并发压测,请求响应慢,使用
mpstat
发现CPU占用率很高,并且占用大多数CPU资源的并不是系统本身,这是个不正常的现象
//mpstat
[root@iZ2ze38tf0alwqik6mufu7Z ~]# mpstat
Linux 3.10.0-1062.1.2.el7.x86_64 (iZ2ze38tf0alwqik6mufu7Z) 12/12/2019 _x86_64_ (1 CPU)
05:33:38 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
05:33:38 PM all 0.25 0.00 0.29 0.01 0.00 0.00 0.00 0.00 0.00 99.46
解决思路
在centos上使用一个很酷的工具 top/htop来查看当前是哪个进程占用了最多的CPU资源,它还可以按照每一个列表头排序,比如这里我用CPU降序显示
在这个案例中,发现最消耗CPU资源的是fork系统调用,而Java语言中只会产生新的线程,而不是进程,那么这一定是发生了外部shell脚本调用
外部shell脚本调用:Java通过Runtime.getRuntime().exec()方法调用,它在JVM中非常的消耗资源,即使外部的shell脚本很快执行完毕,频繁调用时产生的创建进程的开销非常可观,JVM执行这个命令的过程是,首先克隆一个和当前JVM拥有一样的环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程,如果频繁操作嘛,系统消耗极大
fork调用在Linux中是用来产生新++进程++的
那么结果是我们去掉Shell执行的语句,使用JavaAPI来实现相同的效果
学到了
CPU占用率工具 mpstat
详细占用查看工具 top/htop
Java执行外部shell脚本,需要创建进程,消耗较大
案例六 不恰当的数据结构导致内存占用过大
RPC,就是Remote Procedure Call的简称呀,翻译成中文就是远程过程调用
有一个后台RPC服务器,使用64位JVM,内存配置为-Xms4g -Xmx8g -Xmn1g,使用ParNew和CMS,平时YGC时间在30ms以内,但是业务上每10分钟加载一个80MB的数据文件到内存中进行数据分析,这些数据会在内存中形成超过100w个HashMap<Long,Long> Entry,这段时间YGC就会造成500ms的停顿,这个时间就接受不了了
解决思路
首先看这一对收集器ParNew(并行,复制算法)+CMS(标记清除算法),ParNew的高效建立在Eden区对象的朝生夕灭,像这种情况,存活对象太多,复制并维持正确引用负担沉重 我们可以删掉Survivor区,即-XX:SurvivorRetio=65536,即直升老年代,-XX:MaxTenuringThreshold=0 晋升老年代年龄为0,即一次YGC直升老年代 -XX:+AlwaysTenure 表示没有幸存区,所有对象在第一次gc时,会晋升到老年代 但是指标不治本,老年代满了引发FGC
根本原因还是HashMap<Long,Long>这个结构存储文件效率太低 实际耗费的内存未 Long(24B*2)+Entry(32B)+HashMapRef(8B)=88B,空间效率为18% 对象头是8B的倍数时不用填充,所以这里Long 8B MarkWord 8B 指向类元数据8B 当形成Entry 对象头16B next字段 8B int的hashcode 4B 对齐填充 4B Hashmap中对这个entry 8B的引用,这样算出来空间效率不高
学到了
根据收集器先考虑问题,考虑大对象的数据结构空间效率,总是JVM大敌就是大对象,出问题优先考虑他们就对了
对象的组成
- 对象头(Header) 1.1 储存自身运行时数据,即MarkWord(32位32bit,64位64bit) 1.1.1 hash code 1.1.2 GC分代年龄 1.1.3 锁状态标志 1.1.4 线程持有的锁 1.1.5 偏向线程ID 1.1.6 偏向时间戳 1.2 类型指针 对象指向它类元数据的指针
- 实例数据(Instance Data) 真正储存的有效信息
- 对齐填充(Padding) 起到占位符的作用,保证是8字节的整数倍
案例七 由Windows虚拟内存导致的长时间停顿
一个Windows下的GUI桌面程序,每15s会发送一次心跳检测信号,问题是偶尔出现间隔1分钟的无日志输出
解决思路
一看STW了就想到了GC停顿,使用-XX:+PrintGCApplicationStoppedTime配合-Xloggc:gc.log 打印每次GC停顿时间并输出为log,看到确实是gc导致了程序停顿
使用-XX:+PrintReferenceGC可以看到真正GC的启动时间,这里看出从准备开始gc到真正gc,所消耗的时间占了大部分
还有一个特点就是最小化的时候,占用内存大幅度减少,怀疑是最小化的时候,交换到硬盘虚拟的内存空间中去了,这样发生GC就因为恢复页面导致不正常的GC停顿
可以加入参数 "-Dsun.awt.keepWorkingSetOnMinimize"保持一直在内存中,恢复最小化时立即响应.