《深入理解Java虚拟机:JVM高级特性与最佳实践》笔记(三)

虚拟机性能监控与故障处理工具

  • 命令行工具
    • jps:虚拟机进程状况工具
    • jstat:虚拟机统计信息监视工具
    • jinfo:Java配置信息工具
    • jmap:Java内存映像工具
    • jstack:Java堆栈跟踪工具
    • HSDIS:JIT生成代码反汇编
  • 可视化工具
    • jconsole:Java监视与管理控制台
    • jvisualvm:多合一故障处理工具

调优案例分析与实战

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

目前主要有两种方式:

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

64位JDK管理大内存需要考虑的问题:

  1. 内存回收导致的长时间停顿。
  2. 64位JDK的性能测试结果低于32位JDK。
  3. 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照,哪怕产生了快照也几乎无法分析。
  4. 相同程序在64位的JDK消耗的内存一般也比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。

使用逻辑集群来部署程序,可能会遇到以下问题:

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

集群间同步导致的内存溢出

集群同步数据,网络传输带宽不足,重发数据在内存堆积,导致内存溢出。

堆外内存导致的溢出错误

除了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的代码执行也要消耗一定的内存。

外部命令导致系统缓慢

Java执行Runtime.getRuntime().exec()的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大。

服务器JVM进程崩溃

等待的线程和Socket连接数量超过了虚拟机的承受能力。解决办法:生产者-消费者模式。

不恰当的数据结构导致内存占用过大

在HashMap<Long, Long>结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16B(2x8B)。这两个长整型数据包装成java.lang.Long对象之后,就分别具有8B的MarkWord、8B的Klass指针,在加8B存储数据的long值。在这两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int型hash字段,为了对齐,还必须添加4B的空白填充,最后还有HashMap中对这个Entry的8B的引用,这样增加了两个长整型数字,实际耗费内存为(Long(24B)x2)+Entry(32B)+HashMap Ref(8B) = 88B,空间效率为16/88=18%,实在是太低了。

由Windows虚拟内存导致的长时间停顿

Java的GUI程序,当它最小化时,资源管理器中显示的占用内存大幅度减小,但是虚拟内存则没有变化,可能是程序最小化时它的工作内存被自动交换到磁盘的页面文件中了,这样发生GC时就可能因为恢复页面文件的操作而导致不正常的GC停顿。可以加入参数-Dsun.awt.keepWorkingSetOnMinimize=true解决。