乾貨,記一次Metaspace導致頻繁fgc的問題排查過程

最近線上有一條機器在運行了10幾天後出現告警,頻繁出現fgc,在切斷流量之後,從運維那邊拿了應用的heapdump文件。在一開始出現fgc時,我就上了容器平臺查看了gc日誌,gc日誌如下:

fa9fc5fae89643fc9bbdc88be8691b34


從日誌中可以看出很明顯優於metaspace空間不夠造成的fgc,而且不斷進行fgc,且metaspace空間回收不了。於是查看一下jvm啓動參數,參數如下:

5ba16e82e92f4010a144b896dc973ac3


這裏Metaspace和MaxMetaspace都設置成了256M,奇怪了gc日誌中Metaspace才使用了165M就出現了fgc,難道是新加載的類90M的空間嗎,這個可以肯定不是,如果不是新申請90M的空間這個原因引起的,那麼就只有metaspace內存碎片引起的了。於是通過mat分析heapdump,發現 DelegatingClassLoader有1100多個,於是先查看一下 DelegatingClassLoader是個什麼東西?其屬於sun.reflect包下,代碼如下:

classDelegatingClassLoader extendsClassLoader { DelegatingClassLoader(ClassLoader var1) { super(var1); }

證明其確實一個ClassLoader。

那到底是什麼對象在引用這些ClassLoader呢,通過mat發現是 GeneratedMethodAccessor在引用這些ClassLoader,繼續跟蹤發現是mybatis的Reflector應用了這些對象。好辦了,於是繼續查看了Reflector的代碼,代碼片段如下:

privateMap<String, Invoker> setMethods= newHashMap<String, Invoker>();privateMap<String, Invoker> getMethods= newHashMap<String, Invoker>();privateMap<String, Class<?>> setTypes= newHashMap<String, Class<?>>();privateMap<String, Class<?>> getTypes= newHashMap<String, Class<?>>();

這個Reflector對象會緩存orm中實體類的getter setter方法,mybatis需要將表中的記錄轉換成java實體類,爲了提高反射的效率將實體類的方法、構造函數等緩存起來了,Mybatis會在運行的過程中通過 ReflectorFactory爲每一個實體類創建一個 Reflector方便後續進行反射調用。

問題來了,爲什麼會有這麼多的 DelegatingClassLoader呢?通過mat可以分析出來,這些ClassLoader最終都是被java的 Method對象所引用的。

於是分析Method的創建過程和Method的調用過程,最終發現Method在調用過程會創建一個MethodAccessor並將MehtodAccessor作爲存在一個叫做methodAccessor的field中,java爲了提高反射調用的性能,用了一種膨脹(inflation)的方式(從jni調用轉換成classbytes調用),通過參數-Dsun.reflect.inflationThreshold進行控制默認15,在小於這個次數時會使用native的方式對方法進行調用,如果method的調用次數超過指定次數就會使用字節碼的方式生成方法調用,如果使用字節碼的方式最終會爲每一個方法都生成 DelegatingClassLoader。具體的源碼如下:Method.invoke方法:

c0211b84940c4a44adabe8f289ae9d1d


Method.acquireMethodAccessor方法:

ebb06084a4fe4dfa954e954145b927e8


ReflectionFactory.newMethodAccessor方法:

c84e0b0c2bbe4694b7b6e961662f2878


NativeMethodAccessorImpl.invoke方法:

publicObject invoke(Object var1, Object[] var2) throwsIllegalArgumentException, InvocationTargetException { if(++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) { MethodAccessorImpl var3 = (MethodAccessorImpl)(newMethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers()); this.parent.setDelegate(var3); }
 returninvoke0(this.method, var1, var2);}

MethodAccessorGenerator.generateMethod方法片段:

9976334b76c14120aeeb5dd543e57576


ClassDefiner.defineClass方法:

a39d79294075468691842107e1be70ef


另外還有RefectionFactory的checkInitted方法會通過 System.getProperty方法拿 sun.reflect.inflationThresholdproperty,默認值爲15。代碼的流程不是很長,切比較容易理解。接下來就是驗證是不是java反射的Inflat方式引起的。於是寫了下面的例子進行驗證:

/-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCTimeStamps-XX:+PrintGCDetails -Dsun.reflect.inflationThreshold=0/public static voidmain(String[] args) throwsIOException, InvocationTargetException, IllegalAccessException { ReflectorFactory reflectorFactory = newDefaultReflectorFactory(); System.out.println("load class start"); // model有1000個方法Reflector reflector1 = reflectorFactory.findForClass(TestModel.class); Reflector reflector2 = reflectorFactory.findForClass(TestModel2.class); Reflector reflector3 = reflectorFactory.findForClass(TestModel3.class);
 System.out.println("load class finished");
 // model有1000個方法TestModel testModel = newTestModel();
 Object[] empty = {}; Object[] one1 = {"a"};
 TestModel2 testModel2 = newTestModel2();
 TestModel3 testModel3 = newTestModel3();
 System.out.println("method invoke start"); for(inti = 0; i < 1; i++) { for(intj = 0; j < 1000; j++) { reflector1.getSetInvoker("field"+ j).invoke(testModel, one1); reflector1.getGetInvoker("field"+ j).invoke(testModel, empty);
 reflector2.getSetInvoker("field"+ j).invoke(testModel2, one1); reflector2.getGetInvoker("field"+ j).invoke(testModel2, empty);
 reflector3.getSetInvoker("field"+ j).invoke(testModel3, one1); reflector3.getGetInvoker("field"+ j).invoke(testModel3, empty); } } System.out.println("method invoke finished"); System.in.read();}

通過不設置參數 sun.reflect.inflationThreshold和設置參數爲0,運行結果如下:不設置的情況:

213b5d3d58354a588fecbec13e0733a7


設置爲0的情況:

1731cdf20c2e47088992198fe3113218


可以看出兩種設置下Metaspace內存佔用相差很大,基本驗證分析的結果是正確的。最終針對這次因爲Metaspace引起頻繁fgc的修復的方案可以有:

  • 增大Metaspace空間

  • 犧牲一些性能,應用啓動參數中添加參數 -Dsun.reflect.inflationThreshold,並將其值設置的足夠大。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章