在对某个服务进行迁移时,我们观察到数据如下。不同颜色代表不同的机器,可以发现很多机器在不同的时间段内都出现了 GC overhead达到了100%的情况。意味着这段时间内,该机器不能对外提供服务。这是一个很危险的情况,而且并不是偶然。

原因分析
我们找到其中一台机器在GC overhead到达100%时的GC日志,如下:

从日志中可以发现,这两次GC都full GC。在G1中正常情况下是只有young GC 和 mixed GC,full GC 在G1 GC 无法满足内存分配需求时就会切换到serial old GC来收集整个堆内存。严格意义上来讲,full GC 并不属于G1,而是G1无法满足需求时使用的兜底策略。另外,我们还可以从时间戳和执行时间上发现,这两次GC是连续的,并且花费的时间也很长,这就是GC Overhead会升到100%的原因。
第一次GC的原因是Metadata GC Threshold,这表示是由MetaSpace空间不足引起的,而经过第一次GC,Metaspace空间并没有减少,于是引起了第二次GC,第二次GC会尝试清除软引用,但是MetaSpace空间依然没有减少。看到这里,第一反应就是MetaSpace有问题。在JDK 1.8中,为了更灵活的管理内存,永久代被移除,取而代之的是Metaspace。
MetaSpace 不再算是JVM的内存,所以我们在计算内存占用时需要用metaspace+ jvm 的内存。配置永久代的相关参数PermSize以及MaxPermSize也不会在生效了。检查了启动参数之后,当前的metaspace只有512MB。当在大量使用反射,动态代理,动态生成JSP功能时,会导致Metaspace空间不足,导致无法正常回收。
解决方案
了解原因之后,解决起来就很简单,在启动参数中将maxMetaSpaceSize设置为1024MB。改动后 GC Overhead 如下图所示,100%的情况不再出现了。

Metaspace 到底放了什么
既然是Metaspace满了,那我们得看Metaspace里究竟放了什么,我们知道Metaspace里主要存的是类的原始数据,比如我们加载了一个类,那这个类的信息会在Metaspace里分配内存来存储它的一些数据结构,所以大部分情况下,Metaspace的使用量和加载的类个数是关系很大的。上文就是类的数量非常多导致的Metaspace空间不足。
另外还有一种情况Metaspace溢出,是有地方动态构建一个类加载器,同时不断加载一个类,我们遇到过一个案例,通过jmap命令,统计下sun.reflect.DelegatingClassLoader的个数居然达到了几万个
那基本可以锁定是反射类加载器导致Metaspace溢出的原因了,那究竟为什么会有这么多反射类加载器呢,反射类加载器又是什么,接下来先简单说下反射的原理
反射的原理
在java中,反射大部分是这么写,假设有个class A
Method method = A.class.getDeclaredMethod(xx,xx); method.invoke(target,params)
获取Method
要调用首先要获取Method,而获取Method的逻辑是通过Class这个类来的,而关键的几个方法和属性如下:
public Method[] getDeclaredMethods() throws SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
return copyMethods(privateGetDeclaredMethods(false));
}
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
在Class里有个关键的属性叫做reflectionData,这里主要存的是每次从jvm里获取到的一些类属性,比如方法,字段等,这是它在Class类里的定义
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}
这个属性主要是SoftReference的,也就是在某些内存比较苛刻的情况下是可能被回收的,不过正常情况下可以通过-XX:SoftRefLRUPolicyMSPerMB这个参数来控制回收的时机,一旦时机到了,只要GC发生就会将其回收,那回收之后意味着再有需求的时候要重新创建一个这样的对象,同时也需要从JVM里重新拿一份数据,那这个数据结构关联的Method,Field字段等都是重新生成的对象。
后面的博文会讲SoftReferenc 不断加载导致的麻烦。
getDeclaredMethod方法从主要看searchMethods(privateGetDeclaredMethods).privateGetDeclaredMethods返回的方法列表里复制一个Method对象返回。如果reflectionData这个属性的declaredMethods非空,那privateGetDeclaredMethods就直接返回其就可以了,否则就从JVM里load,并赋值给reflectionData的字段。
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
checkInitted();
Method[] res;
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// No cached value available; request value from VM
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicMethods = res;
} else {
rd.declaredMethods = res;
}
}
return res;
}
searchMethods主要调用
getReflectionFactory().copyMethod(res) ->langReflectAccess().copyMethod(arg) ->ReflectAccess.copyMethod->method.copy
由此可见,getDeclaredMethod方法返回的Method对象其实都是一个新的对象,所以不宜调用的过多,如果调用频繁最好缓存起来。
res.root = this 指向reflectionData里被copy 的方法,
//Accessor也是用的被copy那个Method的methodAccessor。
res.methodAccessor = methodAccessormethod
Method调用
private volatile MethodAccessor methodAccessor;
// For sharing of MethodAccessors. This branching structure is
// currently only two levels deep (i.e., one root Method and
// potentially many Method objects pointing to it.)
//
// If this branching structure would ever contain cycles, deadlocks can
// occur in annotation code.
private Method root;
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
root属性其实上面已经说了,主要指向被copyMethod对象,也就是当前这个Method对象其实是根据root这个Method构建出来的,因此存在一个root Method派生出多个Method的情况。
methodAccessor这个很关键了,其实Method.invoke方法就是调用methodAccessor的invoke方法,methodAccessor这个属性如果root本身已经有了,那就直接用root的methodAccessor赋值过来,否则的话就创建一个。
MethodAccessor的实现
public interface MethodAccessor {
/** Matches specification in {@link java.lang.reflect.Method} */
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException;
}
MethodAccessor本身就是一个interface, 有三个实现。
DelegatingMethodAccessorImpl
NativeMethodAccessorImpl
GeneratedMethodAccessorXXX
其中DelegatingMethodAccessorImpl是最终注入给Method的methodAccessor的,也就是某个Method的所有的invoke方法都会调用到这个DelegatingMethodAccessorImpl.invoke。 它是代理类,真正的实现可以是下面的两种
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
void setDelegate(MethodAccessorImpl delegate) {
this.delegate = delegate;
}
}
Default实现是NativeMethodAccessorImpl,而GeneratedMethodAccessorXXX是为每个需要反射调用的Method动态生成的类,后的XXX是一个数字,不断递增的
并且所有的方法反射都是先走NativeMethodAccessorImpl,默认调用ReflectionFactory.inflationThreshold次之后,才生成一个GeneratedMethodAccessorXXX类,生成好之后就会走这个生成的类的invoke方法了
那如何从NativeMethodAccessorImpl过度到GeneratedMethodAccessorXXX呢,来看看NativeMethodAccessorImpl的invoke方法
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
ReflectionFactory.inflationThreshold() default 是15次。我们可以通过-Dsun.reflect.inflationThreshold来指定,我们还可以通过-Dsun.reflect.noInflation=true来直接绕过上面的15次NativeMethodAccessorImpl调用,和
-Dsun.reflect.inflationThreshold=0的效果一样的
而GeneratedMethodAccessorXXX都是通过new MethodAccessorGenerator().generateMethod来生成的,一旦创建好之后就设置到DelegatingMethodAccessorImpl里去了,这样下次Method.invoke就会调到这个新创建的MethodAccessor里了。
动态创建出来的类由DelegatingClassLoader 来加载。请看 ClassDefiner, new DelegatingClassLoader 来加载新类。
class ClassDefiner {
static final Unsafe unsafe = Unsafe.getUnsafe();
/** <P> We define generated code into a new class loader which
delegates to the defining loader of the target class. It is
necessary for the VM to be able to resolve references to the
target class from the generated bytecodes, which could not occur
if the generated code was loaded into the bootstrap class
loader. </P>
<P> There are two primary reasons for creating a new loader
instead of defining these bytecodes directly into the defining
loader of the target class: first, it avoids any possible
security risk of having these bytecodes in the same loader.
Second, it allows the generated bytecodes to be unloaded earlier
than would otherwise be possible, decreasing run-time
footprint. </P>
*/
static Class<?> defineClass(String name, byte[] bytes, int off, int len,
final ClassLoader parentClassLoader)
{
ClassLoader newLoader = AccessController.doPrivileged(
new PrivilegedAction<ClassLoader>() {
public ClassLoader run() {
return new DelegatingClassLoader(parentClassLoader);
}
});
return unsafe.defineClass(name, bytes, off, len, newLoader, null);
}
}
// NOTE: this class's name and presence are known to the virtual
// machine as of the fix for 4474172.
class DelegatingClassLoader extends ClassLoader {
DelegatingClassLoader(ClassLoader parent) {
super(parent);
}
}
每次new类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载。
NativeMethodAccessorImpl.invoke其实都是不加锁的,如果并发很高的时候,可能同时有很多线程进入到创建GeneratedMethodAccessorXXX类的逻辑里,假如有1000个线程都进入到创建GeneratedMethodAccessorXXX的逻辑里,那意味着多创建了999个无用的类,这些类会一直占着内存,直到能回收MetaSpace的GC发生才会回收
怎么发现什么方法在不断反射
sd-jdi.jar来dump.sd-jdi.jar里自带的sun.jvm.hotspot.tools.jcore.ClassDump可以把类的class内容dump到文件里。
ClassDump里可以设置两个System properties:
sun.jvm.hotspot.tools.jcore.filter Filter的类名
sun.jvm.hotspot.tools.jcore.outputDir 输出的目录
首先写一个filter类
import sun.jvm.hotspot.tools.jcore.ClassFilter;
import sun.jvm.hotspot.oops.InstanceKlass;
import sun.jvm.hotspot.tools.jcore.ClassDump;
public class MyFilter implements ClassFilter {
@Override
public boolean canInclude(InstanceKlass kls) {
String klassName = kls.getName().asString();
return klassName.startsWith("sun/reflect/GeneratedMethodAccessor");
}
}
然后编译成class文件
要使用这个首先需要把sa-jdi.jar加到java的classpath里。
进入 刚刚写的filter类的class文件的目录下。执行
java -Dsun.jvm.hotspot.tools.jcore.filter=MyFilter -Dsun.jvm.hotspot.tools.jcore.outputDir=<folder> sun.jvm.hotspot.tools.jcore.ClassDump <pid>
把MyFilter改为你自己的类名,pid为目标 java进程的pid(可以使用jps查看)。然后就会在<folder>产生相应的class文件。
这样我们就可以将所有的GeneratedMethodAccessor给dump下来了,这个时候我们再通过javap -verbose GeneratedMethodAccessor0随便看一个类的字节码

看36行就可以知道到你那个方法导致的Method 反射过多。