内存调优分析
1、高性能硬件上的程序部署策略
在高性能硬件上部署程序主要有两种方式:
① 通过64位JDK来使用大内存
② 适用若干个32位虚拟机建立逻辑集群来利用硬件资源
对于用户交互性强,对停顿时间敏感的系统,给虚拟机分配超大堆的前提是有足够把握将Full GC概
率控制的足够低,例如一天一次出现Full GC,那么可以在固定时间执行定时任务来主动触发。控制
Full GC概率的关键是保证绝大多数对象的存活符合“朝生夕灭”原则,不能有成批量的、长存活的大
对象产生,这样才能保证老年代的空间稳定。在大多数网站形式的应用里,主要对象的生存周期应该
都是请求级或者页面级的,会话和全局级的越少越好。除此之外,使用64位JDK管理大内存还可能有
以下几个问题:
① 内存回收导致长时间停顿。
② 现阶段,64位JDK性能测试普遍低于32位JDK。
③ 需要保证程序足够稳定,产生堆满溢出时很难生成十几GB的Dump文件,生成了也很难分
析
④ 相同程序在64位JDK消耗内存一般比32位的大,这是由于指针膨胀,以及数据类型对齐补
白等因素导致
使用第二种方式的具体做法是在一台物理机上启动多个应用服务器进程,每个进程分配不同端口,然
后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。因为目的仅仅是最大化利用硬件
资源,不需要关心状态保留、热转移之类的高可用需求,也不需要保证每个虚拟机进程有绝对准确的
均衡负载,所以使用无Session复制的亲和式集群是一个很好的选择,只需要保障集群具备亲和性,
也就是均衡器按一定的算法(一般根据SessionID分配)将一个固定的用户请求永远分配到一个固定
的集群节点进行处理即可。使用这种方式可能遇到的问题有以下几点:
① 尽量避免节点竞争全局资源,例如磁盘竞争,各个节点如果同时访问某个磁盘文件很容易
导致IO异常。
② 很难最高效率利用某些资源池,例如连接池,一般都是各个节点建立自己独立的连接池,
有可能导致一些节点池满而另一些还有很多空闲。使用集中式JNDI规避有一定的复杂性也可能
带来额外的性能开销。
③ 各个节点仍然收到32位内存限制,windows下每个进程只能使用2GB内存,考虑到堆外的
内存开销,堆一般最多给到1.5GB,在某些Linux或UNIX系统中,可以提升到3甚至接近4GB,
但最高仍然是4GB。
④ 大量使用本地缓存(如HashMap)的应用,在逻辑集群中会造成较大内存浪费,因为每个
节点都有一份,这时候应该考虑把本地缓存改为集中式缓存。
2、堆外内存导致的溢出错误
Direct Memory并不算入堆内存中,他只能在堆外剩余的内存中划分一块空间。在进行垃圾收集时,
虚拟机虽然会对Direct Memory进行回收,但却不是发现空间不足后立刻执行回收动作,他只能等待
老年代满了后Full GC,然后顺便帮它也清理掉废弃对象,否则他只能一直等到抛出内存溢出异常时
,先catch掉,然后在catch块里要求System.gc()。如果虚拟机仍然没有执行GC动作(例如打开
了-XX:DisableExplicitGC开关),那就只能抛出内存溢出异常了。
除了Java堆和永久代以外,下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进
程最大内存的限制:
① Direct Memory,可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出
OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。
② 线程堆栈,可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,
即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向
无法分配,即无法建立新的线程)。
③ Socket缓冲区,每个Socket连接都Receive和Send两个缓冲区,分别占大约37KB和25KB
内存,连接多的话内存数量可观。如果无法分配,则抛出IOException:Too many open files
异常。
④ JNI代码,如果代码中使用JNI调用本地库,本地库使用的内存也不在堆中。
⑤ 虚拟机和GC,虚拟机、GC的代码执行也需要消耗一定的内存。
3、调整内存设置控制垃圾收集频率
新生代GC频繁一般是由于虚拟机分配给新生代的空间太小导致的,因此有必要使用-Xmm参数来调
大。有些时候内存回收状况不理想,老年代也没有足够的空间,虚拟机就会使用扩容的方式来解决这
个问题,但在扩容之前会先进行Full GC,然后发现空间不足时再去扩容,这个Full GC的时间实际上
是白白浪费了。为了避免这些扩展带来的性能浪费,我们可以把-Xms和-XX:PermSize参数设置为
-Xmx和-XX:MaxpermSize一样的值,在虚拟机启动时就把老年代和永久代容量固定下来。
4、选择收集器降低延迟
在与用户交互频繁的应用中选择合适的收集器很有必要,例如老年代优先使用CMS收集器是最符合的。
可以使用-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC来要求虚拟机在新生代和老年代
分别使用ParNew和CMS两个收集器(使用CMS后,默认新生代使用ParNew,所以也可以不写)。