大内存硬件上的程序部署策略
单个虚拟机管理大内存
出现问题
如果JVM中的堆内存太小,就会频繁地出发GC,而每次GC会将用户线程暂停,所以,频繁地GC会导致长时间的停顿。如果扩大计算的内存的大小,就能降低GC触发的频率。
32位系统最大支持4g内存,而64位操作系统可以最大支持128g内存,所以,我们可以通过换用64位系统和提高内存的方式降低虚拟机的用户线程停顿,但是还是有问题。
虽然Full GC总体的频率降低了,但是每次Full GC的时间却增长了,因为积攒的需要回收的空间变大了嘛。这样会导致长时间的停顿,比以往更要命。
解决方案
对于大内存的硬件,一定要控制Full GC的频率就能减少长时间的停顿,比如一天都不出现一次Full GC,可以在深夜没有用户使用的时候进行一次。
控制Full GC频率的关键是老年代的相对稳定,如何控制老年代稳定:
- 保证绝大多数对象是朝生夕灭的,大多数对象的生存时间不能太长,尤其是大对象。
- 提高大对象进入老年代的门槛,这样可以让对象先在新生代被Minor GC掉。
调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存。另外建立一个Apache服务作为前端均衡代理作为访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,处理器资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比起硬件升级前有较大提升。
单个虚拟机管理大内存的问题
- 回收大内存耗时长,G1出现后才有所改善。
- 64位虚拟机性能低于32位的。
- 法在堆内存溢出的时候,如果,堆过大就无法产生堆转存储快照,转了也很难分析,太大了
- 64位虚拟机内存消耗过大,因为指针膨胀、数据对齐。
一台物理机上建立虚拟机逻辑集群管理内存
同时使用若干个虚拟机建立逻辑集群来利用硬件资源。做法是在一台物理机器上启动多个应用服务器进程,为每个服务器进程分配不同端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。
存在问题
- 磁盘竞争,多个节点同时访问一个磁盘文件,导致IO异常。
- 很难高效利用资源池。可能一个还有很多,另一个已经满了。
- 如果使用32位系统,内存空间太小。
- 造成内存的浪费,可以把本地缓存改为集中式缓存。
直接内存的溢出
问题描述
服务端不定时抛出内存溢出异常,将堆内存调到最大,还是有这个问题。加入-XX:+HeapDumpOnOutOfMemoryError参数,居然也没有任何反应,再用jstat查看GC堆,也很正常。说明不是堆内存溢出。系统的内存是2g,而1.6g分给了Java堆,直接内存只剩下0.4g。如果使用了NIO,那么JVM会在JVM内存之外分配内存空间,这部分内存也叫“直接内存”。因此,如果程序中使用了NIO,那么就要小心所以是直接内存异常。
直接内存的回收
虚拟机对直接内存不能像Java堆那样,发现空间不足就触发GC,而是在老年代满了之后的Full GC顺便清理直接内存。所以,会出现直接内存溢出。
虚拟机进程崩溃
异步请求
web服务器采用HTTP通信,而HTTP基于TCP。异步通信就是当客户端向服务器发送一个HTTP请求后,将这个请求的TCP连接委托给其他线程,自己去做其他的事情,那个被委托的线程就等你这服务器的反馈。当这个线程接收到了反馈,就将数据转交给刚才的线程,这就是异步通信。
异步请求导致进程奔溃
使用了异步方式调用web服务,由于两边的服务速度完全不对等,时间长了就会累计越来越多的服务请求,从而导致等到的线程和socket连接越来越多,就会超过虚拟机的承受能力,就会崩溃。
解决方法
将异步调用改成生产者消费折模式的消息队列。
大量大对象进入堆
问题描述
一下子有太多太大的对象进入新生代,而暂时又不能被回收,由于新生代中采用的标记复制算法,这样导致Minor GC的效率低,同时会很快占满新生代,频繁触发GC,从而导致停顿。
解决方法
修改进入老年代的年龄限制,让对象马上进入老年代。
本质问题还是要减少大对象,提高空间利用效率。
这个案例中由于使用了HashMap 作为长整型的数据存储结构,这样的空间使用效率非常低。
安全点导致的长时间停顿
问题描述
当个别线程没有进入安全点,而其他线程都都在安全等待的时候,就会导致长时间的停顿。
当线程在一个循环中,可能需要等待循环全部跑完才能进入安全点,这样其他线程也必须一起等着。
解决方法
把循环索引中的数据类型从int改为long。
处理大对象
大对象对于虚拟机是一个非常棘手的问题。可能会导致频繁的GC,占用大量空间,延长GC的时间。
处理方法
- 避免大对象的产生:选择空间效率较高的数据结构存储大的信息。
- 缩短大对象的生存时间:尽快丢掉大对象,这样就能避免对象占用空间,多次被复制所消耗的时间。