JVM监控问题排查

一、JVM运行时数据区
 从上图可以看出JVM运行时内存区域的划分。下面分别来介绍一下:
1、程序计数器
    程序计数器中,主要作用是通过改变这个数值来选取下一条需要执行的指令。例如,线程切换之后,需要读取该线程程序计数器中的值来恢复现场。所以,改内存区域是线程独享的。
2、虚拟机栈&本地方法栈
    JVM会为每个线程分配一个vm stack,然后线程中的每个方法都是一个stack flame(栈帧),方法的执行就是stack flame的入栈和出栈。stack flame中主要存储的就是:局部变量表、操作数栈、动态链接、方法出口等信息。通过vm stack的作用就能看出,  VM stack是线程独享的,和线程拥有相同的生命周期。
    在我们常用的sun hotspot虚拟机中,把native method stack合并到了vm stack,其实他们的作用相似。VM stack是为虚拟机执行java方法服务,而native method stack是为虚拟机使用到的native方法服务。
3、java堆(java heap)
    虚拟机管理内存中最大的一块。被所有线程共享。这块内存的唯一目的就是存放实例化的对象。细化一点可以分作新生代(young generation)和老年代(old generation)。然后新生代还划分为:一个Eden区和两个survivor区。以上是咱们常用的hotspot的划分方式,不同的虚拟机,有不同的方式。
4、方法区(method area)
    主要用于存放被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。这也是被所有线程 共享的。它有一个别名叫non heap,也就是为了区分堆的意思。
    方法区包括:运行时常量池(runtime constant pool)、直接内存(direct memory)。
    方法区一般是划分到永久带,或者metaspace。
二、垃圾收集器
 上图解释一下,如果两个收集器有连线,说明可以搭配使用。垃圾收集器所处区域表示他是新生代还是老年代的收集器。这些收集器的具体细节暂不做详细讲解,日后再专门做讲解。只要记住有这些收集器就行,以后遇到了一个了解一个就OK。
三、内存分配回收策略
    我总结出来就下面这句话 :对象优先进入Eden区,在多次GC之后达到上限的对象就会进入老年代。而大对象直接进入老年代。然后还有有动态对象年龄判断(如果survivor中相同年龄对象大小总和大于survivor空间一般,年龄大于等于该年龄的对象直接进入老年代)。
四、JDK命令
1、jps(jvm process status)
     jps [option] [hostid]
   用来查看基于 HotSpot 的 JVM 里面中,所有具有访问权限的 Java 进程的具体状态 ,  包括进程 ID ,进程启动的路径及启动参数等等,使用 jps 时,如果没有指定 hostid ,它只会显示本地环境中所有的 Java 进程;如果指定了 hostid ,它就会显示指定 hostid 上面的 java 进程,不过这需要远程服务上开启了 jstatd 服务。
option取值:

hostid语法
[protocol:][[//]hostname][:port][/servername]
protocol -  如果 protocol 及 hostname 都没有指定,那表示的是与当前环境相关的本地协议,如果指定了 hostname 却没有指定 protocol ,那么 protocol 的默认就是 rmi 。
hostname -  服务器的 IP 或者名称,没有指定则表示本机。
port -  远程 rmi 的端口,如果没有指定则默认为 1099 。
Servername -  注册到 RMI 注册中心中的 jstatd 的名称。
2、jstat(jvm statistics monitoring tool)
 jstat [option vmid [interval[s|ms] [count]] ]
vmid:当前java进程号
interval:查询间隔时间,默认单位为ms。可以通过m&ms来指定单位。
count:查询次数,默认查询一次。
例如:jstat -gc 111 10 10则表示,查询进程ID为111的垃圾收集情况,每隔10ms查询一次,查询 10次。
option参数如下表:
 命令执行样例

不同的统计维度(statOption)及输出说明

    -class

    -gcutil

    从应用程序启动到采样时发生 Full GC 的次数

    -gcpermcapacity

    -gcoldcapacity

    -gcold

    -gcnewcapacity

    -gcnew

     这个选项用于查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因,它比-gcutil会多出最后一次垃圾收集原因以及当前正在发生的垃圾收集的原因。

    -gccause

    -gccapacity

     -gc

     -compiler

类加载情况的统计
列名说明
Loaded加载了的类的数量
Bytes加载了的类的大小,单为Kb
Unloaded卸载了的类的数量
Bytes卸载了的类的大小,单为Kb
Time花在类的加载及卸载的时间
HotSpot中即时编译器编译情况的统计
列名说明
Compiled编译任务执行的次数
Failed编译任务执行失败的次数
Invalid编译任务非法执行的次数
Time执行编译花费的时间
FailedType最后一次编译失败的编译类型
FailedMethod最后一次编译失败的类名及方法名
JVM中堆的垃圾收集情况的统计
列名说明
S0C新生代中Survivor spaceS0当前容量的大小KB)
S1C新生代Survivor space中S1当前容量的大小(KB)
S0U新生代Survivor space中S0容量使用的大小(KB)
S1U新生代Survivor space中S1容量使用的大小(KB)
ECEden space当前容量的大小(KB)
EUEden space容量使用的大小(KB)
OCOld space当前容量的大小(KB)
OUOld space使用容量的大小(KB)
PCPermanent space当前容量的大小(KB)
PUPermanent space使用容量的大小(KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
YGCT从应用程序启动到采样时 Young GC 所用的时间(秒)
FGC从应用程序启动到采样时发生 Full GC 的次数
FGCT从应用程序启动到采样时 Full GC 所用的时间(秒)
GCTT从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC
新生代、老生代及持久代的存储容量情况
列名说明
NGCMN新生代的最小容大小(KB)
NGCMX新生代的最大容量大小KB)
NGC当前新生代的容量大小KB)
S0C当前新生代中survivor space 0的容量大小KB)
S1C当前新生代中survivor space 1的容量大小KB)
ECEden space当前容量的大小(KB)
OGCMN老生代的最小容量大小(KB)
OGCMX老生代的最大容量大小(KB)
OGC当前老生代的容量大小(KB)
OC当前老生代的空间容量大小(KB)
PGCMN持久代的最小容量大小(KB)
PGCMX持久代的最大容量大小KB)
PGC当前持久代的容量大小KB)
PC当前持久代的空间容量大小(KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
FGC从应用程序启动到采样时发生 Full GC 的次数
用于查看垃圾收集的统计情况,包括最近发生垃圾的原因
列名说明
LGCC最后一次垃圾收集的原因,可能为unknown GCCause”、“System.gc()”等
GCC当前垃圾收集的原因
新生代垃圾收集的情况
列名说明
S0C当前新生代中survivor space 0的容量大小(KB)
S1C当前新生代中survivor space 1的容量大小(KB)
S0US0已经使用的大小(KB)
S1US1已经使用的大小(KB)
TTTenuring threshold,要了解这个参数,我们需要了解一点Java内存对象的结,在Sun JVM中,(除了数组之外的)对象都有两个机器字(words)的头部。第一个字中包含这个对象的标示哈希码以及其他一些类似锁状态和等标识信息,第二个字中包含一个指向对象的类的引用,其中第二个字节就会被垃圾收集算法使用到。
在新生代中做垃圾收集的时候,每次复制一个对象后,将增加这个对象的收集计数,当一个对象在新生代中被复制了一定次数后,该算法即判定该对象是长周期的对象,把他移动到老生代,这个阈值叫着tenuring threshold。这个阈值用于表示某个/些在执行批定次数youngGC后还活着的对象,即使此时新生的的Survior没有满,也同样被认为是长周期对象,将会被移到老生代中。
MTTMaximum tenuring threshold,用于表示TT的最大值。
DSSDesired survivor size (KB).可以参与这里:http://blog.csdn.net/yangjun2/article/details/6542357
ECEden space当前容量的大小(KB)
EUEden space已经使用的大小(KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
YGCT从应用程序启动到采样时 Young GC 所用的时间(单位秒)
新生代的存储容量情况
列名说明
NGCMN          新生代的最小容量大小(KB)
NGCMX    新生代的最大容量大小KB)
NGC    当前新生代的容量大小KB)
S0CMX新生代中SO的最大容量大小KB)
S0C当前新生代中SO的容量大小KB)
S1CMX新生代中S1的最大容量大小KB)
S1C当前新生代中S1的容量大小KB)
ECMX新生代中Eden的最大容量大小KB)
EC当前新生代中Eden的容量大小KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
FGC从应用程序启动到采样时发生 Full GC 的次数
老生代及持久代发生GC的情况
列名说明
PC当前持久代量的大小(KB)
PU持久代使用容量的大小(KB)
OC当前老年代容量的大小(KB)
OU老年代使用容量的大小(KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
FGC从应用程序启动到采样时发生 Full GC 的次数
FGCT从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC
老生代的存储容量情况
列名说明
OGCMN老生代的最小容量大小(KB)
OGCMX老生代的最大容量大小(KB)
OGC当前老生代的容量大小KB)
OC当前新生代的空间容量大小KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
FGC从应用程序启动到采样时发生 Full GC 的次数
FGCT从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC
持久代的存储容量情况
列名说明
PGCMN持久代的最小容量大小(KB)
PGCMX持久代的最大容量大小(KB)
PGC当前持久代的容量大小KB)
PC当前持久代的空间容量大小KB)
YGC从应用程序启动到采样时发生 Young GC 的次数
FGC
FGCT从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC
新生代、老生代及持代垃圾收集的情况
列名说明
S0Heap上的 Survivor space 0 区已使用空间的百分比
S1Heap上的 Survivor space 1 区已使用空间的百分比
EHeap上的 Eden space 区已使用空间的百分比
OHeap上的 Old space 区已使用空间的百分比
PPerm space 区已使用空间的百分比
YGC从应用程序启动到采样时发生 Young GC 的次数
YGCT从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC从应用程序启动到采样时发生 Full GC 的次数
FGCT从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

    -printcompilation

HotSpot编译方法的统计
列名说明
Compiled编译任务执行的次数
Size方法的字节码所占的字节数
Type编译类型
Method指定确定被编译方法的类名及方法名,类名中使名“/”而不是“.”做为命名分隔符,方法名是被指定的类中的方法,这两个字段的格式是由HotSpot中的“-XX:+PrintComplation”选项确定的。


3、jinfo(configuration info for java)
实时查看和调整虚拟机各项参数。
4、jmap(memory map for java)
jmap [option] vmid
用于生成堆转储快照,一般称为dump文件或者heapdump文件。
option值以及含义:
 例如要生成111java进程的 heap快照。
jmap -dump:live,format=b,file=test.d 111
5、jstack(stack trace for java)
用于生成当前时刻线程快照。命令格式:
jstack [option] vmid
option可选项:
 
以上是常用的JVM调试的命令,多用用就会了。描述的越详细就说明约常用。
五、调优案例分析
最近公司的交易量相对之前有很大提升。但是发现,系统每天到了晚上的时候就会出现CPU使用率升高,内存告警等。(为什么是晚上,我稍后说)首先我们查看源码,排查之后并没有发现问题。然后开始我们的JVM问题排查调优。
1、首先,我们通过jps命令查看当先系统中运行的java进程。
 可以看到,当前这台Linux机器上只有3409这个java进程。
2、通过top -hp 3409命令查看当前进程内最消耗CPU的线程。
 这里的截图并不是当时发生问题时候的截图,所以自行脑补有一条CPU使用率90+的线程咯。。哈哈
3、用 printf   "%x\n"  3454得到3454线程的十六进制,我们假设为54aa(这里是假设的,实际的自行操作获取)。
4、用jstack获取3454进程的堆栈信息,并且通过管道符过滤54aa线程的。
jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread"  prio=10 tid=0x00007f950043e000 nid=0x54ee  in  Object.wait() [0x00007f94c6eda000]
上述代码是网上截取的,实际以自己操作为准。然后我们就可以看到占用cpu最高的那个类名了: PollIntervalRetrySchedulerThread,这个类中有一个等待锁的操作,导致CPU飙升。
5、然后再通过jmap -heap 3409来获取当前堆使用 情况
 
 由上图可以看出当前当前Eden、survivor、old、perm区的使用情况。这里可以看到,老年代使用其实不高,只有30%多。因为这是极其正常的时候截图。当时我发现问题的时候,老年代使用率超过90%。所以引发了full GC,所以才会CPU飙升。
6、然后我们再使用jmap -histo:live 3409|head -n 20查看前20个对象数。
 可以看出, 某个 java 进程(使用 pid )内存内的,所有 对象 的情况(如:产生那些对象,及其数量)。帮助我们定位问题对象。同样,这里还是用某台正确的机器代替, 当晚某一个对象的数量爆炸,大概有百万级别的对象数量。找到这个对象是一个加密对象BouncyCastleProvider。查看代码发现:
 
     
  1. Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",
  2. new BouncyCastleProvider());
这里是一个加密对象,发现每次使用cipher都会new BouncyCastleProvider(),对象。然后这个对象的实现方法,发现里面有很多map。所以导致,这个对象实例化的时候,每次都很大。然后直接进入老年代,无法进行monitor GC。只有进行full GC的时候才会清除掉这些对象。具体 BouncyCastleProvider类中的代码为节省篇幅,只截取一小段。
 
      
  1. public BouncyCastleProvider()
  2. {
  3. super(PROVIDER_NAME, 1.40, info);
  4. for (int i = 0; i != SYMMETRIC_CIPHERS.length; i++)
  5. {
  6. Class clazz = null;
  7. try
  8. {
  9. clazz = Class.forName(SYMMETRIC_CIPHER_PACKAGE + SYMMETRIC_CIPHERS[i] + "Mappings");
  10. }
  11. catch (ClassNotFoundException e)
  12. {
  13. // ignore
  14. }
  15. if (clazz != null)
  16. {
  17. try
  18. {
  19. addMappings((Map)clazz.newInstance());
  20. }
  21. catch (Exception e)
  22. { // this should never ever happen!!
  23. throw new InternalError("cannot create instance of "
  24. + SYMMETRIC_CIPHER_PACKAGE + SYMMETRIC_CIPHERS[i] + "Mappings : " + e);
  25. }
  26. }
  27. }
  28. //
  29. // X509Store
  30. //
  31. put("X509Store.CERTIFICATE/COLLECTION", "org.bouncycastle.jce.provider.X509StoreCertCollection");
  32. put("X509Store.ATTRIBUTECERTIFICATE/COLLECTION", "org.bouncycastle.jce.provider.X509StoreAttrCertCollection");
  33. put("X509Store.CRL/COLLECTION", "org.bouncycastle.jce.provider.X509StoreCRLCollection");
  34. put("X509Store.CERTIFICATEPAIR/COLLECTION", "org.bouncycastle.jce.provider.X509StoreCertPairCollection");
  35. put("X509Store.CERTIFICATE/LDAP", "org.bouncycastle.jce.provider.X509StoreLDAPCerts");
  36. put("X509Store.CRL/LDAP", "org.bouncycastle.jce.provider.X509StoreLDAPCRLs");
  37. put("X509Store.ATTRIBUTECERTIFICATE/LDAP", "org.bouncycastle.jce.provider.X509StoreLDAPAttrCerts");
  38. put("X509Store.CERTIFICATEPAIR/LDAP", "org.bouncycastle.jce.provider.X509StoreLDAPCertPairs");
  39. //
  40. // X509StreamParser
  41. //
  42. put("X509StreamParser.CERTIFICATE", "org.bouncycastle.jce.provider.X509CertParser");
  43. put("X509StreamParser.ATTRIBUTECERTIFICATE", "org.bouncycastle.jce.provider.X509AttrCertParser");
  44. put("X509StreamParser.CRL", "org.bouncycastle.jce.provider.X509CRLParser");
  45. put("X509StreamParser.CERTIFICATEPAIR", "org.bouncycastle.jce.provider.X509CertPairParser");
  46. //
  47. // KeyStore
  48. //
  49. put("KeyStore.BKS", "org.bouncycastle.jce.provider.JDKKeyStore");
  50. put("KeyStore.BouncyCastle", "org.bouncycastle.jce.provider.JDKKeyStore$BouncyCastleStore");
  51. put("KeyStore.PKCS12", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$BCPKCS12KeyStore");
  52. put("KeyStore.BCPKCS12", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$BCPKCS12KeyStore");
  53. put("KeyStore.PKCS12-DEF", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$DefPKCS12KeyStore");
  54. put("KeyStore.PKCS12-3DES-40RC2", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$BCPKCS12KeyStore");
  55. put("KeyStore.PKCS12-3DES-3DES", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$BCPKCS12KeyStore3DES");
  56. put("KeyStore.PKCS12-DEF-3DES-40RC2", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$DefPKCS12KeyStore");
  57. put("KeyStore.PKCS12-DEF-3DES-3DES", "org.bouncycastle.jce.provider.JDKPKCS12KeyStore$DefPKCS12KeyStore3DES");
  58. put("Alg.Alias.KeyStore.UBER", "BouncyCastle");
  59. put("Alg.Alias.KeyStore.BOUNCYCASTLE", "BouncyCastle");
  60. put("Alg.Alias.KeyStore.bouncycastle", "BouncyCastle");  
  61. //这里还有很多类似的put操作,大概500个左右吧。省略了
  62. }
7、先说一下这个案例的解决方案。

将上述Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",

new BouncyCastleProvider());

更改为Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",

"BC");

然后说一下,抛开这次案例不讲。假设上述操作还是没有发现问题,那么接下来进行下面操作。

8、jmap -dump:live,format=b,file=test.d 3409

通过获取dump 文件来分析。具体分析我推荐使用MAT。具体使用方式待以后详细说明。


后记
这里说一下永久区的一些问题。我们知道,jvm内存划分如下:
新生代
    Eden区
    survivor区(2个)
老年代
永久代
方法区主要是划归永久代,因为这里的GC往往效果低下。但是在JDK1.8以后, 在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?

  元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。以下几点原因:摘自(https://www.cnblogs.com/paddix/p/5309550.html)

  1、字符串存在永久代中,容易出现性能问题和内存溢出。

  2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  4、Oracle 可能会将HotSpot 与 JRockit 合二为一。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值