实际运行中的解决方案

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

概述

对于交互性强、对停顿时间敏感的系统,可以给Java虚拟机分配超大堆的前提是有把握把应用程序的Full GC频率控制的足够低,至少要低到不会影响用户使用。例如以天为频率就可以通过在深夜执行定时任务来触发Full GC甚至自动重启应用服务器来保持内存可用空间在一个稳定的水平。

控制Full GC频率的关键在于大多数对象的生存时间不应太长,尤其是不能有大批的、长生存时间的大对象产生,这样才能保证老年代的稳定。

方案

在高性能硬件上部署程序,目前主要有两种方式:

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

64位JDK来管理大内存

问题

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

32位集群

方法

一台物理机器上启动多个应用服务器进程,采用不同的端口,前端搭建负载均衡器,反向代理分配请求,使用无session的亲和式集群。

亲和式集群:均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的请求永远分配到固定的一个节点处理。

由于一台物理机器,所以无需考虑下列问题:

  • 状态保留
  • 热转移
  • 精准的负载均衡

问题

  • 尽量避免节点竞争全局的资源
    • 磁盘竞争:各个节点如果同时反问某个磁盘文件(并发写操作),很容易导致IO异常
  • 很难最高效率地利用某些资源池
    • 连接池:一般都是在各个节点建立独立的连接池,这会导致一些节点池满了,而另外一个节点仍有较多空余。尽管可以使用集中式JNDI,但又一定复杂性并且可能带来额外的性能开销
  • 各个节点仍然不可避免的受到32位的内存限制
    • 在32位Windows平台中每个进程只能使用2GB的内存,考虑到堆外内存开销,堆一般最多只能开到1.5G。在某些Linux或UNIX系统中,可以提升到3G乃至接近4G的内存,但32位中仍受到最高4G内存的限制
  • 大量使用本地缓存
    • 大量使用HashMap作为K/V缓存的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点都有一份缓存,可以考虑把本地缓存改为集中式

堆外内存导致的溢出错误

问题

1
2
3
4
[org.eclipose.jetty.util.log] handle fialed java.lang.OutOfMemoryError:null
at sun.misc.Unsafe.allocateMemory(Native Method)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)

说明

NIO操作需要使用到Direct Memory内存,垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory只能等待老年代满了后Full GC,然后顺便清理内存的废弃对象。不然只能等到抛出内存溢出异常时,先catch掉,再在catch块里调用System.gc()。如果-XX:DisableExplicitGC被打开,虚拟机会忽略掉手动调用GC的代码,使得 System.gc()的调用就会变成一个空调用,完全不会触发任何GC,就只能在堆中还有许多空闲内存时,抛出内存溢出异常了。

补充

除了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:执行时会消耗一定内存

外部命令导致系统缓慢

如果Java程序执行时需要调用外部Shell脚本,通过Runtime.getRuntime().exec()虽然可以达到目的,但在虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程的开销也非常可观。

Java虚拟机执行这个命令的过程是:

  • 克隆一个和当前虚拟机拥有一样环境变量的进行
  • 用新的进程执行命令
  • 退出这个进程

频繁执行时,系统的消耗会很大,不仅是CPU,内存负担也很重

如果Shell脚本仅仅是例如获取系统信息这种Java API可以做到的事情,改为用Java API去获取这些信息更好。