本文重点讲述毕玄大师在其公众号上发的一个GC问题一个jstack/jmap等不能用的case,对于毕大师那篇文章,题目上没有提到GC的那个问题,不过进入到文章里能够看到,既然文章提到了jstack/jmap的问题,这里也简单回答下jstack/jmap没法使用的问题,其实最多见的场景是使用jstack/jmap的用户和目标进程不是同一个用户,哪怕你执行jstack/jmap的动做是root用户也无济于事,不过毕大师这里主要提到的是jmap -heap/histo这两个参数带来的问题,若是使用-heap/histo的参数,其实和你们使用-F参数是同样的,底层都是经过serviceability agent来实现的,并非jvm attach的方式,经过sa连上去以后会挂起进程,在serviceability agent里存在bug可能致使detach的动做不会被执行,从而会让进程一直挂着,能够经过top命令验证进程是否处于T状态,若是是说明进程被挂起了,若是进程被挂起了,能够经过kill -CONT [pid]来恢复。算法
再回到那个GC的问题,用的参数以下:数据结构
demo程序以下:jvm
执行效果以下函数
发现gc的时间愈来愈长,可是gc触发的时机以及回收的效果都差很少,那问题究竟在哪里呢?工具
虽然这个demo代码逻辑很简单,可是其实这是一个特殊的demo,并不简单,若是咱们将XStream对象换成Object对象,会发现不存在这个问题,既然如此那有必要进去看看这个XStream的构造函数:oop
这个构造函数仍是很复杂的,里面会建立不少的对象,上面还有一些方法实现我就不贴了,总之都是在不断构建各类大大小小的对象,一个XStream对象构建出来的时候大概好像有 12M 的样子。测试
那究竟是哪些对象会致使 ygc 不断增加呢,因而可能想到逐步替换上面这些逻辑,好比将最后一个构造函数里的那些逻辑都禁掉,而后咱们再跑测试看看还会不会让ygc不断恶化,最终咱们会发现,若是咱们直接使用以下构造函数构造对象时,若是传入的classloader是AppClassLoader,那会发现这个问题再也不出现了。3d
测试代码以下:日志
gc日志以下:code
是否是以为很神奇,因而可知,这个classloader相当重要。
这里着重要说的两个概念是初始类加载器
和定义类加载器
。举个栗子说吧,AClassLoader->BClassLoader->CClassLoader,表示AClassLoader在加载类的时候会委托BClassLoader类加载器来加载,BClassLoader加载类的时候会委托CClassLoader来加载,假如咱们使用AClassLoader来加载X这个类,而X这个类最终是被CClassLoader来加载的,那么咱们称CClassLoader为X类的定义类加载器,而AClassLoader和BClassLoader分别为X类的初始类加载器,JVM在加载某个类的时候对这三种类加载器都会记录,记录的数据结构是一个叫作SystemDictionary的hashtable,其key是根据ClassLoader对象和类名算出来的hash值,而value是真正的由定义类加载器加载的Klass对象,由于初始类加载器和定义类加载器是不一样的classloader,所以算出来的hash值也是不一样的,所以在SystemDictionary里会有多项值的value都是指向同一个Klass对象。
那么JVM为何要分这两种类加载器呢,其实主要是为了快速找到已经加载的类,好比咱们已经经过AClassLoader来触发了对X类的加载,当咱们再次使用AClassLoader这个类加载器来加载X这个类的时候就不须要再委托给BClassLoader去找了,由于加载过的类在JVM里有这个类加载器的直接加载的记录,只须要直接返回对应的Klass对象便可。
咱们的demo里发现构建了一个CompositeClassLoader的类加载器,那到底有没有用这个类加载器加载类呢,咱们能够设置一个断点在CompositeClassLoader的loadClass方法上,因而看到下面的堆栈:
可见确实有类加载的动做,根据类加载委托机制,在这个demo中咱们能确定类是交给AppClassLoader来加载的,这样一来CompositeClassLoader就变成了初始类加载器,而AppClassLoader会是定义类加载器,都会在SystemDictionary里存在,所以当咱们不断new XStream的时候会不断new CompositeClassLoader对象,加载类的时候会不断往SystemDictionary里插入记录,从而使SystemDictionary愈来愈膨胀,那天然而然会想到若是GC过程不断去扫描这个SystemDictionary的话,那随着SystemDictionary不断膨胀,那么GC的效率也就越低,抱着验证下猜测的方式咱们可使用perf工具来看看,若是发现cpu占比排前的函数若是都是操做SystemDictionary的,那就基本验证了咱们的说法,下面是perf工具的截图,基本证明了这一点。
想象一下这么个状况,咱们加载了一个类,而后构建了一个对象(这个对象在eden里构建)当一个属性设置到这个类里,若是gc发生的时候,这个对象是否是要被找出来标活才行,那么天然而然咱们加载的类确定是咱们一项重要的gc root,这样SystemDictionary就成为了gc过程当中的被扫描对象了,事实也是如此,能够看vm的具体代码:
看上面的SH_PS_SystemDictionary_oops_do task
就知道了,这个就是对SystemDictionary进行扫描。
可是这里要说的是虽然有对SystemDictionary进行扫描,可是ygc的过程并不会对SystemDictionary进行处理,若是要对它进行处理须要开启类卸载的vm参数,CMS算法下,CMS GC和Full GC在开启CMSClassUnloadingEnabled的状况下是可能对类作卸载动做的,此时会对SystemDictionary进行清理,因此当咱们在跑上面demo的时候,经过jmap-dump:live,format=b,file=heap.bin
命令执行完以后,ygc的时间瞬间降下来了,不过又会慢慢回去,这是由于jmap的这个命令会作一次gc,这个gc过程会对SystemDictionary进行清理。
很遗憾hotspot目前没有对ygc的每一个task作一个时间的统计,所以没法直接知道是否是SHPSSystemDictionaryoopsdo这个task致使了ygc的时间变长,为了证实这个结论,我特意修改了一下代码,在上面的代码上加了一行:
而后从新编译,跑咱们的demo,测试结果以下:
咱们会发现YGC的时间变长的时候,SystemDictionaryOOPSDO的时间也会相应变长多少,所以验证了咱们的说法。