前言
学以致用,至此JVM中关于内存部分的理论知识与工具的使用,我们已经搞定了。
那么接下来,本文,将重点就如何运用这些知识与工具来进行故障处理和调优进行叙述。
64位Java虚拟机
采用64位JDK来管理大内存,需要注意的问题:
- 产生堆溢出几乎无法产生堆转储快照,哪怕产生了快照也几乎无法进行分析,因为dump文件会很大,达到十几个G。
- 相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对其补白等因素导致的。
- 我们可以采用在一台物理机上启动多个应用服务器进程,若干个32位虚拟机建立的逻辑集群来利用硬件资源。可以采用无Session复制的亲和式集群,也就是将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可。
当然这种一台物理机上建立无Session复制的亲和式集群的多个应用的方式,也有它的缺点:
- 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争,尤其是并发写同一文件,很容易导致IO异常。
- 很难最高效率的利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点连接池满了,另外一些节点仍有较多空余。
- 各个节点仍然不可避免的收到32位的内存限制,每个进程最多只能使用4GB的内存,考虑到堆以外的开销,每个进程肯定实际使用的内存比4GB小。
- 大量使用本地缓存,在逻辑集群中会造成较大的浪费,因为每个逻辑节点上都有一份缓存,这时候可以考虑把本地缓存改为集中式缓存。
被集群共享的数据要使用类似JBossCache这种集群缓存来同步的话,可以允许读操作频繁,因为数据在本地内存有一份副本,读取的动作不会耗费多少资源,但不应当有过于频繁的写操作,那样会带来很大的网络同步的开销。
某些情况下-XX:HeapDumpOnOutOfMemoryError没反应,抛出内存溢出异常时什么文件都没有生成。我们可以使用原生的命令jstat -gcutil vmid 间隔 次数来监控GC的情况。
Direct Memory
垃圾收集时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收。
它只能等待老年代满了后Full GC,然后”顺便的“帮它清理掉内存的废弃对象。
否则它只能一直等到抛出内存溢出异常时,先catch掉,再在catch块里面”大喊“一声:”System.gc( )!“。
要是虚拟机打开了**-XX:+DisableExplictGC**开关,那就只能静静的看着堆中还有很多空闲内存,自己却不得不抛出内存溢出异常了。
堆外内存之总结
从实践的角度出发,除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存。这里所有内存的总和受到操作系统进程最大内存的限制:
- Direct Memory:可以使用**-XX:MaxDirectMemorySize**调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError: Direct buffer memory。
- 线程堆栈:可以通过**-Xss**调整大小,内存不足时,抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。
- Socket 缓存区:每个Socket连接都有Receive和Send两个缓冲区,连接多的话这块内存占用也比较乐观。如果无法分配,则可能会抛出IOException:Too many open files异常。
- JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。
- 虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。
Connection reset
对方接口无法正常调用,如果采用异步的方式,会使我们的服务不断的积累着等待的线程,最后超过虚拟机的承受能力后使得虚拟机进程崩溃,解决办法,通知对方修复接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正常。
-XX:+PrintGCApplicationStoppedTime:打印GC过程中应用程序被阻塞的时间。
-XX:+PrintGCDateStamps
-Xloggc:gclog.log
从GC日志文件中确认停顿是否有GC导致的。
-XX:+PrintReferenceGC:可以使我们从GC日志中找到长时间停顿的具体日志信息。
什么是JIT即时编译器
我们看到的上图中的编译时间到底是什么呢?程序在运行之前不是已经编译好了吗?
这里的编译时间就是指虚拟机的JIT编译器编译热点代码的耗时,我们知道Java语言为了实现跨平台的特性,Java编译出来的是字节码,虚拟机通过解释的方式执行字节码指令。
为了解决程序解释执行的速度问题,虚拟机内置了两个运行时编译器。
如果一段Java代码方法被调用次数达到一定程度,就会被判定为热代码交给JIT编译器即时编译为本地代码,提高运行速度,这也就是HotSpot虚拟机名字的由来。
Java的运行期编译最大的缺点就是它进行编译需要消耗程序正常的运行时间,这也就是上面所说的编译时间。
屏蔽掉System.gc( ),-XX:+DisableExplictGC。
VisualVM的实战分析
如下图所示,我们分析下VisualVM中的VisualGC插件中的元素代表的意义:
- 这是我们的插件的名字。
- 我们变异了629次,花费了514ms。
- 加载了1610个类,花费了444ms。
- 共进行了1次垃圾收集,耗费了7ms。上一次发生GC的原因是内存不够使用。
- Eden区共8M,使用了7.62M,发生了一次Minor GC,花费了7ms。
- 幸存区1共1M,还没被占用。
- 幸存区1共1M,占用1K。
- 老年代共10M,已被占用了8M,没有发生Full GC。
- 代码区1G,占用了6M。
新生代共10M,其中非用户时间由三部分组成:
- GC时间
- JIT编译时间
- 类加载时间
内存溢出排查之总结
通过上面的工具的学习,以及一节理论结合的实践,当虚拟机出现反应缓慢时,我们可以进行以下的排查:
- 采用的什么垃圾收集器?
- 然后看下GC的情况,如Full GC的次数以及停顿的时间等。
- 以及是否有大对象,一直存活的情况等。
- 如果我们可以将应用程序的Full GC频率控制得足够低,譬如几天出现一次Full GC,那么我们就可以通过在深夜执行定时任务的方式触发Full GC、甚至自动重启应用服务器来保持内存可用空间在一个稳定的水平。
- 如果CPU资源敏感度低,可以考虑CMS收集器进行垃圾回收,能够并发的进行垃圾回收。
如果感觉比较零散,不好记住的话,可以参考以下几点:
- 看虚拟机参数,有个大概的了解配置的情况。
- 看内存快照,分析其中内存的占用情况,是否有大量大对象存活。
- 分析GC的过程,看是否不是正常的内存回收。
- 根据线程快照,看线程的运行情况,线程是否有死锁或者被某些资源阻塞的情况。
- 分析编译时间与类加载时间是否比较正常。