深入理解Java虚拟机::调优案例分析与实战

调优案例分析与实战

  • 大内存硬件上的程序部署策略
    • 目前单体应用在较大内存的硬件上主要的部署方式有两种:
      • 通过一个单独的 Java 虚拟机实例来管理大量的 Java 堆内存。
      • 同时使用若干个 Java 虚拟机,建立逻辑集群来利用硬件资源。
    • 面临的问题
      • 回收大块堆内存而导致的长时间停顿,自从 G1 收集器的出现,增量回收得到比较好的应用
      • 大内存必须有 64 位 Java 虚拟机的支持,但由于压缩指针、处理器缓存行容量(Cache Line)等因素,64 位虚拟机的性能测试结果普遍略低于相同版本的 32 位虚拟机。
      • 必须保证应用程序足够稳定,因为这种大型单体应用要是发生了堆内存溢出,几乎无法产生堆转 储快照,哪怕成功生成了快照也难以进行分析;如果确实出了问题要进行诊断,可能就必须应用 JM C 这种能够在生产环境中进行的运维工具。
      • 相同的程序在 64 位虚拟机中消耗的内存一般比 32 位虚拟机要大,这是由于指针膨胀,以及数据类 型对齐补白等因素导致的,可以开启压缩指针功能来缓解。
  • 集群间同步导致的内存溢出
    • 集群同步软件的问题导致了内存泄漏
    • -XX:+HeapDumpOnOutOfMemoryError
  • 堆外内存导致的溢出错误
    • 直接内存:可通过-XX:MaxDirectMemorySize 调整大小,内存不足时抛出 OutOfMemoryError 或者 OutOfMemoryError:Direct buffer memory。
    • 线程堆栈:可通过-Xss 调整大小,内存不足时抛出 StackOverflowError(如果线程请求的栈深度大 于虚拟机所允许的深度)或者 OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时 无法申请到足够的内存)。
    • Socket 缓存区:每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接 多的话这块内存占用也比较可观。如果无法分配,可能会抛出 IOException:Too many open files 异常。
    • JNI 代码:如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚 拟机的本地方法栈和本地内存的。
    • 虚拟机和垃圾收集器:虚拟机、垃圾收集器的工作也是要消耗一定数量的内存的。
  • 外部命令导致系统缓慢
    • 执行这个 Shell 脚本是通过 Java 的 Runtime.getRuntime().exc()方法来调用的。这种调用方式可以达到执行 Shell 脚本的目的,但是它在 Java 虚拟机中是非常消耗资源的操作,即使外 部命令本身能很快执行完毕,频繁调用时创建进程的开销也会非常可观。
  • 服务器虚拟机进程崩溃
    • 由于 MIS 系统的用户多,待办事项变化很快,为了不被 OA 系统速度拖累,使用了异步的方式调用 Web 服务,但由于两边服务速度的完全不对等,时间越长就累积了越多 Web 服务没有调用完成,导致在等待的线程和 Socket 连接越来越多,最终超过虚拟机的承受能力后导致虚拟机进程崩溃。通知 OA 门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正常。
  • 不恰当数据结构导致内存占用过大
    • 业务上需要每 10 分钟加载一个约 80M B 的数据文件到内存进行数据分析,这些数据会在内存中形成超过 100 万个 HashMap<Long,Long>Entry,在这段时间里面 Minor GC 就会造成超过 500 毫秒的停顿,对于这种长度的停顿时间就接受不了了
    • 如果不修改程序,仅从 GC 调优的角度去解决这个问题,可以考虑直接将 Survivor 空间去掉(加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0 或者-XX:+Always-Tenure),让新生代中存活的对象在第一次 Minor GC 后立即进入老年代,等到 Major GC 的时候再去清理它们。
  • 由 Windows 虚拟内存导致的长时间停顿
    • 程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了,这样发生垃圾收集时就有可能因为恢复页面文件的操作导致不正常的垃圾收集停顿。
    • 在 Java 的 GUI 程序中要避免这种现象,可以加入参数“- Dsun.awt.keepWorkingSetOnMinimize=true”来解决
  • 由安全点导致长时间停顿
    • 解决问题的第一步是把这两个特别慢的线程给找出来,这个倒不困难,添加-XX: +SafepointTimeout 和-XX:SafepointTimeoutDelay=2000 两个参数,让虚拟机在等到线程进入安全点的时间超过 2000 毫秒时就认定为超时,这样就会输出导致问题的线程名称
    • 方法调用、循环跳转、异常跳转这些位置都可能会设置有安全点,但是 HotSpot 虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用 int 类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop ),相对应地,使用 long 或者范围更大的数据类型作为索引值的循环就被称为不可数循环 (Uncounted Loop),将会被放置安全点。通常情况下这个优化措施是可行的,但循环执行的时间不单单是由其次数决定,如果循环体单次执行就特别慢,那即使是可数循环也可能会耗费很多的时间。
    • 清理连接的索引值就是 int 类型,所以这是一个可数循环,HotSpot 不会在循环中插入安全点。当垃圾收集发生时,如果线程刚好执行到该函数里的可数循环时,则必须等待循环全部跑完才能进入安全点,此时其他线程也必须一起等着,所以从现象上看就是长时间的停顿。找到了问题,解决起来就非常简单了,把循环索引的数据类型从 int 改为 long 即可,但如果不具备安全点和垃圾收集的知识,这 种问题是很难处理的。