`

JVM参数设置、性能监控及gc分析

    博客分类:
  • jvm
阅读更多

一、JVM参数的使用

1、Xms与Xmx

Xms:JVM启动时初始化堆内存的大小

Xmx:JVM分配的堆内存的最大值

Xms设置的值过小,可能会导致应用启动时内存不够,从而应用启动失败,Xmx值过小,可能会导致应用启动后运行一段时间,内存不够用,一般设置Xmx大小为总机器内存的80%。同时将Xms的值和Xmx的值设置为一样,从而减少系统新增heap内存带来的性能损耗。

 

错误:java.lang.OutOfMemoryError. Java heap space

JVM在启动的时候会自动设置JVM Heap的值,Heap的大小是Young Generation 和Tenured Generaion 之和。 在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。此时分2种情况,因为某个JVM线程申请不到足够的一块大内存而抛出异常,JVM进程内其它线程能够正常访问,只是当前线程OOM;另外一种是某个JVM线程连很小内存都申请不到,所有堆内存都被用光,进行full gc都不能回收内存,此时会导致整个JVM进程退出,java.lang.OutOfMemoryError: GC overhead limit exceeded就是这种情况,可以使用-XX:-UseGCOverheadLimit 去掉JVM默认的参数设定,但最后JVM还是因为OOM会退出。此时可以通过Thread.setDefaultUncaughtExceptionHandler()方法来保留当前JVM线程退出前的未捕捉到的错误信息到指定文件(保留现场),但是当前JVM线程必定会因为抛出的error而退出的。当所有非守护线程都退出时,JVM就会退出!

 解决方法:修改JVM Heap的设置即Xms Xmx

 

2、XX:PermSize与XX:MaxPermSize

这2个参数分别是持久代的初始化大小和最大值,是用来存放加载的class及meta元素,如果应用加载的类较多,则需要调大一些。

错误:java.lang.OutOfMemoryError. Java PermGen space

sun的GC不会在主程序运行期对PermGen space进行清理,所以如果你的APP会载入很多CLASS的话,就很可能出现PermGen space溢出。

解决方法:修改XX:MaxPermSize的大小

 

3、Xss

这个参数表示分配给每个线程的栈的大小,JDK5.0以后默认都是分配1M的空间,一般这个值足够用了

错误:java.lang.StackOverFlowError

通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间(这个大约相当于在一个C函数内声明了256个int类型的变量),那么栈区也不过是需要1MB的空间。通常栈的大小是1-2MB的。通常是程序的递归层次太多,或者出现了死循环(笔者就遇到过),才会报这个错。

解决办法:可以先试着把Xss的值调到2M,如果还没有解决问题就要修改程序了。如果不容易查出问题所在,可以dump线程下来分析,本文后面会介绍。

 

4、需要注意的是,当线程栈所需要的空间申请不到,也就是说内存不够时,也会报out of memory的错误。还有一种情况不管是申请堆空间还是申请栈空间,当空间不够需要抛出异常,而保存异常信息也是需要空间的,所以当此时不够空间来收集异常信息,此时的JVM就会crash。这是一个临界的状况,需要注意。

       以上是常用的几个JVM参数,其它的参数需要了解 可以参考:

http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

 

二、JVM性能监控

1、JVM自带的常用命令

 

JPS:查看机器上的JVM进程,也可以指定ip,进行远程连接,但必须要有权限,默认端口1099

该命令会显示JVM进程的id和名字

 

Jstat :查看具体某个JVM进程的具体状态

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

解释如下:

Option 包括以下选项:

-class

-compiler

-gc

-gccapacity

-gccause

-gcnew

-gcnewcapacity

-gcold

-gcoldcapacity

-gcpermcapacity

-gcutil

-printcompilation

 

Vmid 就是 jps 查看到的进程 id ,如 Jserver 的进程 id 是 15117 。

Interval 是时间间隔,单位为毫秒, 1000 就是一秒。

Count 就是需要查看的次数。

 

例子假设我们需要查看 172.25.1.24 机器 vmid 为 15117 的 gc 的情况,可以输入下面的命令:

jstat -gc 15117@172.25.1.24 1000 3

jstack:linux特有,查看某个线程的具体情况

 

jmap:打印出某个java进程(使用pid)内存内的所有‘对象’的情况(如:产生那些对象,及其数量)。

用法:

jmap [ option ] pid

jmap [ option ] executable core

jmap [ option ] [server-id@]remote-hostname-or-IP

常用参数:

-dump:[live,]format=b,file=<filename> 使用hprof二进制形式,输出jvm的heap内容到文件=. live子选项是可选的,假如指定live选项,那么只输出活的对象到文件. 

-finalizerinfo 打印正等候回收的对象的信息.

-heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况.

-histo[:live] 打印每个class的实例数目,内存占用,类全名信息. VM的内部类名字开头会加上前缀”*”. 如果live子参数加上后,只统计活的对象数量. 

示例:jmap -dump:format=b,file=test.bin 4939

一般dump下来的数据需要使用工具查看,可参考:http://blog.csdn.net/fenglibing/article/details/6298326

也可使用visualVM分析JVM内存

visualVM的JVM远程连接机器监控VM

        visualVM在JDK6.0 update 7 中自带,java启动时不需要特定参数,监控工具在bin/jvisualvm.exe),能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的。
需要配置如下:
JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true  -Dcom.sun.management.jmxremote.port=1090 -Dcom.sun.management.jmxremote.ssl=false  -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=
 -Djava.rmi.server.hostname=$HOST_NAME"
上述配置了JMX远程连接的端口、需要认证、指定用户和密码存储的文件
然后从指定的文件中找到用户名和密码,然后打开visulVM远程连接该机器

 

三、GC分析

先看一下HotSpot VM的对内存结构如下:

 

java中新建的对象都是用新生代分配内存,新生代由Eden和Survivor组成。Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。旧生代用于存放新生代中经过多次垃圾回收仍然存活的对象。每次jvm垃圾回收会将新生代的无用内存回收,如果gc前后heap内存回收总量比新生代回收的总量要少,说明有内存被提升到老年代和永久区中,通常以此依据来分析内存泄露问题。

新生代GC一般是使用复制算法进行清理,因此按照复制算法的原理将新生代分成了3个区域:Eden、Survivor0、Survivor1。Hotspot虚拟机的3个空间缺省配比为:8:1:1,jvm只会使用eden和1个survivor作为新生代空间.当新生代空间不足时发生minor gc,此时根据复制算法, jvm会首先将eden和from survivor中存活的对象拷贝到to survior中,然后释放eden和from中的所有需要回收对象,最后调换from/to survior,jvm将eden和新的from survior作为新生代。当然上述minor gc顺利执行还取决于很多因素,这里只描述了最理想化的状态。

下面举例说明:



 上面截图的是连续2次一般的gc,一旦eden区已满(gc发生频繁内存泄露,从而导致增加内存补充eden区都不够用),或者
Permanet Generation满 了都会发生full gc,而full gc的时候JVM要暂停所有进程,所以频繁full gc是绝对不允许出现在生成环境的。

先看上图第一次gc:1642004K->4067K(1843200K:指的是eden区+S0), 0.0041120 secs] 1750731K->112796K(3989504K:young+old), 0.0043270 secs]

表示eden区及S0从1642004K到回收后4067K,总共大小1843200K,回收了1637937K(eden+S0被回收的空间)的空间,heap从1750731K到112796K,总共大小3989504K,回收了1637953K(eden+S0+S1被回收的空间),说明有16K的对象(来自S0区)被升到tenure区或perm区了。

 

同样看接下来的一次gc: 1642467K->4342K(1843200K), 0.0066650 secs] 1751196K>113086K(3989504K), 0.0069010 secs]

这次eden区回收了1638125K, heap回收了1638110K,有15K数据被提升了。

虽然每次都有10多K的数据被提升了但是它的gc时间和heap的总大小并没有变化,所以暂时还没有什么内存泄露。上述是单点的分析,当怀疑系统内存泄露时不仅要单点分析还需要进行周期性观察jvm的gc和full gc的情况,类似上面的分析过程一样。

    不能仅仅凭短时间的几个抽样比较,因为对于抽样来说,Full GC前后的区别,运行时长的区别,资源瞬时占用的区别都会影响判断。同时要结合Full GC发生的时间周期,每一次GC收集所耗费的时间作为辅助判断标准。

         顺便说一下,Heap YoungGen,OldGen,PermGen的设置也是需要注意的,并不是越大越好,越大执行收集的时间越久,但是可能执行Full GC的频率会比较低,因此需要权衡。这些仔细的去了解一下GC的基础设计思想会更有帮助,不过一般用默认的也不错。还有就是可以配置一些特殊的GC,并行,同步等等,充分利用多CPU的资源。

 

四、内存泄露

 

程序中要注意的,经常可能出现内存泄露的地方:

1、ThreadLocal
       它的使用是典型的用空间换时间,为了避免使用同步带来的性能损耗,而为每个线程都创建一份副本,这样就很容造成内存空间的泄露。
2、 Collection
        集合里面会存储一组数据,特别当定义的集合是静态的时候,它会常驻内存,必须得记得在不需要集合的时候将内存释放掉
3、listeners and callbacks
        监听器是被注册在特定的对象事件上,一旦特定对象事件不再被使用时,需要记得将监听器的注册解除掉。
4、Dead loop
        死循环,肯定会消耗大量的内存,直至用光最后内存溢出

 

5、Data result from database
          当从数据库请求获取大量数据时,容易撑爆内存
五、本地内存泄露排查

本地内存的占用包括这么几种:

  • java.nio.ByteBuffer#allocateDirect 使用Unsafe操作本地内存的DirectByteBuffer
  • java.nio.channels.FileChannel#map 它实际上也是返回的DirectByteBuffer实例
  • 通过JNI调用c/c++等代码,主要指native方法
  • jvm进程本身消耗的内存
  • 依赖的一些so库(linux上)使用的内存

DirectByteBuffer

这个占用本地内存的大小可以通过著名的MaxDirectMemorySize进行限制。其原理可以通过翻看java.nio.DirectByteBuffer#DirectByteBuffer(int) 构造函数得知。 因此也是最容易排除掉的一种泄露

另外,D.B.B 默认是通过PhantomReference机制释放内存的

可以简单的理解为,java.nio包里对本地内存的常规使用,都是可以通过MaxDirectMemorySize进行限制的。 所谓常规使用,是相对于通过反射访问一些package/private 权限的API的使用方式而言的。 后面会稍微提到下这种方式。

native方法泄露

其实D.B.B 最终也是通过native方法操作本地内存,但是因为可以通过M.D.M.S 参数限制,因此单独作为一种场景来讲。native方法泄露一般是指malloc了但是没有正确的free的场景。

这里出镜率最高的恐怕是 Deflater/Inflater 了。起初排查这个东西,只是照着葫芦画瓢不大理解其中原理,葫芦在这(http://bluedavy.me/?p=205)。其实严格来说,它只是回收不及时,不能算是真正的泄露,因为Xflater实现了finalize方法,借助jvm的Finalizer机制回收内存。但是Finalizer在高并发的情况下,容易回收不及时,只有一个最多两个线程呼哧呼哧的做finalize调用,(http://www.oracle.com/technetwork/java/javamail/finalization-137655.html)Oracle官方这篇文章给出了如何尽量降低、避免finalize内存滞留问题的详细讨论。native方法导致泄露这种场景的通用招式就是,使用googleperf/扁鹊之类的工具追踪记录内存分配情况

简单介绍下gperf的使用方法:

前两个是linux的动态库机制,具体含义不展开,可以谷歌之。第三个是googleperf使用的环境变量,用来定义吐出的文件的路径前缀。可以将上述三个export放到启动脚本里,这样可以避免bash环境变量的集成问题

运行一段时间后,会在/home/admin/perftools目录下生成test打头的文件: test.0001.heap/test.0002.heap,也许还有test_.0001.heap ...,后者推断是有子进程的情况下生成的。

使用perftools/bin/pprof 工具可以对生成的heap数据进行各种姿势的分析,最基本的产出内存占比:

% pprof --text gfs_master /tmp/profile.0100.heap
   255.6  24.7%  24.7%    255.6  24.7% GFS_MasterChunk::AddServer
   184.6  17.8%  42.5%    298.8  28.8% GFS_MasterChunkTable::Create
   176.2  17.0%  59.5%    729.9  70.5% GFS_MasterChunkTable::UpdateState
   169.8  16.4%  75.9%    169.8  16.4% PendingClone::PendingClone
    76.3   7.4%  83.3%     76.3   7.4% __default_alloc_template::_S_chunk_alloc
    49.5   4.8%  88.0%     49.5   4.8% hashtable::resize
   ...

The first column contains the direct memory use in MB.
The fourth column contains memory use by the procedure and all of its callees.
The second and fifth columns are just percentage representations of the numbers in the first and fourth columns.
The third column is a cumulative sum of the second column (i.e., the kth entry in the third column is the sum of the first k entries in the second column.)

那么,通过对第四列排序,就得到了一个调用链上某方法及其被调用方法的内存占用情况。

实际中,更典型的结果可能类似这种:

     0.0   0.0% 100.0%      6.4   5.4% 0x00007f6c29dfc4e6
     0.0   0.0% 100.0%      6.4   5.4% 0x00007f6c29e0370f
     0.0   0.0% 100.0%      6.9   5.9% JavaCalls::call_helper
     0.0   0.0% 100.0%     46.9  39.6% ArrayAllocator::allocate
     0.0   0.0% 100.0%     46.9  39.7% universe_post_init
     0.0   0.0% 100.0%     48.6  41.1% init_globals
     0.0   0.0% 100.0%     48.8  41.3% JNI_CreateJavaVM
     0.0   0.0% 100.0%     48.8  41.3% Threads::create_vm
     0.0   0.0% 100.0%     51.9  43.9% 0x00007f6c2a5d6608
     0.0   0.0% 100.0%     51.9  43.9% Java_java_lang_UNIXProcess_forkAndExec
     0.0   0.0% 100.0%     54.1  45.7% __clone
     0.0   0.0% 100.0%     54.2  45.8% start_thread
     0.0   0.0% 100.0%     67.9  57.4% AllocateHeap
   109.7  92.8%  92.8%    109.7  92.8% os::malloc

除了C/C++函数,还会看到一些0x地址。。。。这是什么鸟,怎么看? 答案是暂时没法看!JVM的同学正在做功能,不久的明天就能从地址映射到函数名称了。

但是, 联想到JNI在java 函数声明和C/C++名称上是有修饰规则的:
Java_package_Class_method
有了这个,就能定位到对应的java方法了。 再明确一下,gperf trace到的内存信息能看到某方法本身及其调用的方法占用的虚拟内存之和(第4列)。
上图是个真实的案例,非常缓慢的内存泄露,两周天600m的样子。然后对比分析一段时间跨度的test.xxx.heap ,发先0x00007f6c2a5d6608这个函数消耗一直在涨,关键点在于它相邻的的Java_java_lang_UNIXProcess_forkAndExec,占用大小、变化情况是那么的相似。。。按照java native修饰规则,这个对应 java.lang.UNIXProcess.forkAndExec 方法,那么这样就回到了我们熟悉的Java 领域,具体到这个案例,考虑到创建进程这么有特点的功能,一问开发就知道了,是调用了shell,读取系统的文件参数,并且每分钟80多下,也就是80多个进程,两周这个数量也是蛮喜人的。。。于是元凶算是抓到了

依赖的so库

这个也是一个真实线上场景的案例。这个只简单描述下,开下脑洞,细节数据当时犯懒也没记录下来。应用使用了Inflater压缩数据,反复创建,并发度较高。 前面也提到,Inflater/Deflater在未及时调用end的是时候会导致内存泄露,但是这个经过初步改造之后,已经都加上了end的调用,理论上不应该再有滞留问题,并且通过jmap -histo:live 也证实Finalizer都已执行完毕,btrace跟踪方法, 与end方法调用数量相等,进一步证明没有未调用end的情况。但是rss依然居高不下,这时楼主就没辙了,然后咨询了 @云达 , 据说之前发现过malloc释放内存不及时的情况,将jvm的依赖的malloc库替换成jemalloc后问题解决。 然后才知道了linux的malloc如今已经不是简单的分配、释放内存,也有一套复杂的机制。。。 这里也是产生了碎片问题,这篇文章描述了这个问题:malloc的内存用free释放后为何系统回收不了。 博主也是个linux外行,这里不做展开,仅作为一种实际存在的case,开下脑洞,细节自己挖:)

JVM自身bug

JVM自身的bug也是五花八门的,只是想到了这种可能,或者其他文档中有提到这种可能,还没有处理过实际案例。

  • 大小: 47.6 KB
  • 大小: 76.1 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics