- tags:反射
- categories: problems
- date: 2017-05-28 14:50:04
使用反射代理類加載器的潛在內存使用問題
大量的類加載器 “sun/reflect/DelegatingClassLoader”,用來加載“sun/reflect/GeneratedMethodAccessor”類,可能導致潛在的占用大量本機內存空間問題,應用服務器進程占用的內存會顯著增大。您還有可能遇到拋出的內存溢出錯誤。案例:假笨說-從一起GC血案談到反射原理
先把結論說明了:
在上述案例中,得到的結論是:反射類加載器導致Perm溢出。系統使用jdk1.7,且GC收集器是G1。這個版本的G1的特性是只有在Full GC的情況下才會對perm(永久區)里的類進行卸載,正常的G1的gc過程是不會對Perm中的類進行卸載的,所以,當Perm內存被類堆積滿的時候,就會進行一次Full Gc將無用的類卸載掉。那么為什么會產生那么多類在Perm區域中呢,通過案例中GC日志的分析可以知道,是因為產生了大量的"sun.reflect.DelegatingClassLoader",那么為什么會有那么多的代理委托類加載器,用于加載什么類的呢?
從分析可以知道,是因為使用三方的Xfire協議,該協議過程中產生大量的RPC,將得到結果進行反序列化時候,是通過Method.invoke反射原理來實現目的的,在Xfire實現中,內部還有Methodref等包含軟索引SoftReference的引用,很容易就會被G1給回收了,一旦回收了,程序內部就會Copy來創建一個新的Method,在調用其invoke通過MethodAccessor來實際調用,多次操作大于指定閾值(15)就會創建一個“sun/reflect/GeneratedMethodAccessor”類字節碼,然后在通過DelegatingClassLoader來加載該字節碼,就會將這些類都保存到Perm中,如此復返,就會Perm溢出。-
當使用Java反射時,Java虛擬機有兩種方法獲取被反射的類的信息。它可以使用一個JNI存取器。如果使用Java字節碼存取器,則需要擁有它自己的Java類和類加載器(sun/reflect/GeneratedMethodAccessor類和sun/reflect/DelegatingClassLoader)。這些類和類加載器使用本機內存。字節碼存取器也可以被JIT編譯,這樣會增加本機內存的使用。如果Java反射被頻繁使用,會顯著地增加本機內存的使用。
Java虛擬機會首先使用JNI存取器,然后在訪問了同一個類若干次后,會改為使用Java字節碼存取器。這種當Java虛擬機從JNI存取器改為字節碼存取器的行為被稱為膨脹。幸運的是,我們可以通過一個Java屬性控制這種行為。屬性sun.reflect.inflationThreshold會告訴Java虛擬機使用JNI存取器多少次。如果設為0,則總是使用JNI存取器。由于字節碼存取器比JNI存取器使用更多本機內存,當我們看到大量Java反射時,最好使用JNI存取器。我們只需要設置inflationThreshold屬性值為0即可。
image
Reflection反射原理
下面通過一個簡單的反射例子來捋一捋java內部反射機制過程。
public class ReflectDemo {
public static void main(String[] args) throws Exception{
Proxy target = new ReflectDemo.Proxy();
Method method = Proxy.class.getDeclaredMethod("pmethod", null);
//MethodAccessor.invoke
method.invoke(target, null);
}
static class Proxy{
public void pmethod(){
System.out.println("Proxy.pmethod");
}
}
}
上述可以看到通過Class.getDeclaredMethod反射方法獲取Proxy類中pmethod方法,最后成功調用。先從下圖中看看從方法調用到最后方法執行的流程圖:
獲取得到Method對象
根據上面的例子,當通過Class.getDeclaredMethod(MethodName)進行反射,獲取指定類的指定的方法的時候,具體是如何進行的?結合上面的流程圖,分步說明:
// Class.java
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// be very careful not to change the stack depth of this
// checkMemberAccess call for security reasons
// see java.lang.SecurityManager.checkMemberAccess
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;
}
可以看到,先調用privateGetDeclaredMethods方法,在調用searchMethods方法最后得到一個Method對象。
其中,在看到privateGetDeclaredMethods方法之前,需要知道Class類中有個很重要的屬性:ReflectionData,這個屬性類中就是保存著每次從JVM中獲取指定類時候類中的屬性,比如方法,屬性字段等:
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;
//value of classRedefineCount when we create this reflectionData instance
final int redefinedCount;
ReflectionData(int redefinedCount){
this.redefinedCount = redefinedCount;
}
}
這個屬性是軟引用SoftReference的,也就是在某些內存比較緊張的情況下,是會被GC回收的,可以通過JVM參數:-XX:SoftRefLRUPolicyMSPerMB來控制回收時機。所以,一旦某個類的ReflectionData屬性被回收了的話,意味著當第二次再想通過這個屬性來獲取反射信息的時候,就會發現,緩存中沒有了,就會重新創建一個新的ReflectionData對象,將從JVM中獲取到類的信息封裝到屬性中,那么這個類的屬性類中的關聯所有Method,Field等屬性對象都是重新創建的。重新創建對象必然會消耗內存資源和一些時間,有一些副作用也是肯定的。
然后,現在新的JDK版本,將ReflactionData對象給取代了,取代方式是在Class.java類中通過聲明反射的軟引用集合屬性:
//java.lang.Class
/**
* Reflection support.
*/
// Caches for certain reflective results
private static boolean useCaches = true;
private volatile transient SoftReference<Field[]> declaredFields;
private volatile transient SoftReference<Field[]> publicFields;
private volatile transient SoftReference<Method[]> declaredMethods;
private volatile transient SoftReference<Method[]> publicMethods;
private volatile transient SoftReference<Constructor<T>[]> declaredConstructors;
private volatile transient SoftReference<Constructor<T>[]> publicConstructors;
// Intermediate results for getFields and getMethods
private volatile transient SoftReference<Field[]> declaredPublicFields;
private volatile transient SoftReference<Method[]> declaredPublicMethods;
// Incremented by the VM on each call to JVM TI RedefineClasses()
// that redefines this class or a superclass.
private volatile transient int classRedefinedCount = 0;
// Value of classRedefinedCount when we last cleared the cached values
// that are sensitive to class redefinition.
private volatile transient int lastRedefinedCount = 0;
(1) privateGetDeclaredMethods方法:
從緩存或者JVM中獲取該Class中,符合反射方法調用傳遞過出來方法名稱,方法參數類型的Method對象列表。
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
checkInitted();
Method[] res = null;
if (useCaches) {
clearCachesOnClassRedefinition();
if (publicOnly) {
if (declaredPublicMethods != null) {
res = declaredPublicMethods.get();
}
} else {
if (declaredMethods != null) {
res = declaredMethods.get();
}
}
if (res != null) return res;
}
//若是在當前反射緩沖中,所有的Method,Field軟引用數組中都沒有找到,
//則調用Reflection.filterMethods方法從JVM內部獲取,將得到的數據
//封裝到Class的反射引用字段數組中,緩存起來
// No cached value available; request value from VM
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (useCaches) {
if (publicOnly) {
declaredPublicMethods = new SoftReference<>(res);
} else {
//重新緩存
declaredMethods = new SoftReference<>(res);
}
}
return res;
}
(2) searchMethods方法調用:
searchMethods將從privateGetDeclaredMethods返回的方法列表里找到一個同名的匹配的方法,然后復制一個新的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;
}
//拷貝一個Method對象
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
(3) ReflectionFactory.copyMethod()方法:
當在第二步中,在方法列表中匹配到同名同參數的Method對象的時候,這時候就調用ReflectionFactory.copyMehthod方法,拷貝一個一樣方法。在ReflectionFactory中有個LangReflectAccess字段,用于解決反射訪問其他包中,私有的,共有的方法或者字段屬性的權限問題。
//ReflectionFactory.java
// Provides access to package-private mechanisms in java.lang.reflect
private static volatile LangReflectAccess langReflectAccess;
/** Makes a copy of the passed method. The returned method is a
"child" of the passed one; see the comments in Method.java for
details. */
public Method copyMethod(Method arg) {
return langReflectAccess().copyMethod(arg);
}
(4) ReflectAccess.copyMethod方法:
上述第三步驟中ReflectionFactory.copyMethod中的langReflectionAccess()方法就是返回一個LangReflectAccess接口類,而ReflectAccess則是該接口的實現類,所以就會調用該類的copyMethod()方法:
//
// Copying routines, needed to quickly fabricate new Field,
// Method, and Constructor objects from templates
//
public Method copyMethod(Method arg) {
return arg.copy();
}
可以,最后實質是調用傳入的參數Method對象的copy方法。
(5) Method.copy()方法:
Method對象的copy方法,主要是通過將傳入的Method對象的方法名,參數,等等其他屬性都拷貝一份,但是Method對象的methodAccessor字段卻是共享的。通過Method類中root字段屬性來實現。
//Method.java
// 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.)
private Method root;
//共享的methodAccessor
private volatile MethodAccessor methodAccessor;
Method copy() {
// This routine enables sharing of MethodAccessor objects
// among Method objects which refer to the same underlying
// method in the VM. (All of this contortion is only necessary
// because of the "accessibility" bit in AccessibleObject,
// which implicitly requires that new java.lang.reflect
// objects be fabricated for each reflective call on Class
// objects.)
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
// Might as well eagerly propagate this if already present
res.methodAccessor = methodAccessor;
return res;//返回拷貝的Method對象
}
調用Method.invoke方法
通過上面的步驟,就能將目標類的指定反射方法的拷貝對象給獲取到了。就如例子中Method method = Proxy.class.getDeclaredMethod("pmethod", null)
這句執行完成了。下面就是調用方法的過程了。
整個Method.invoke方法內部實際調用是MethodAccessor接口實現類的invoke方法調用。Method.invoke只是表面的殼而已,也可以說是代理調用。下面就說說MethodAccessor接口實現類和實際調用過程。
MethodAccessor接口的聲明:
public interface MethodAccessor {
/** Matches specification in {@link java.lang.reflect.Method} */
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException;
}
該接口的實現類有以下幾種:
- NativeMethodAccessorImpl
- DelegatingMethodAccessorImpl
- GeneratedMethodAccessorXXX
其中的第一個NativeMethodAccessorImpl對應的就是通過JNI(java本地接口)存取器來獲取反射類字節碼信息。第三個GeneratedMethodAccessor<Num> 類就是通過代理類加載器DelegatingClassLoader來加載反射類字節碼的。中間的代理MethodAccessorImpl則是可以將傳入的MethodAccessor參數對象,注入給Method對象的methodAccessor屬性,實現代理調用:也就是說明,通常情況下,我們調用某個Method對象的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;
}
}
(1) Method.invoke方法調用:
我們代碼顯示調用的Method對象的invoke對象,雖然已經知道實際上是MethodAccessor實現類底層調用,但是也會是可以看看該方法內部源代碼:
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
// Until there is hotspot @CallerSensitive support
// can't call Reflection.getCallerClass() here
// Workaround for now: add a frame getCallerClass to
// make the caller at stack depth 2
Class<?> caller = getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
//Method類共享methodAccessor對象,判斷是否為null,若是null,則調用指定方法獲取
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
//調用以下方法來獲取MethodAccessor實現類
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
(2) acquireMethodAccessor方法獲取MethodAccessor:
當某個反射方法的methodAccessor屬性字段為null的時候,需要顯示的調用acquireMethodAccessor方法來獲取方法訪問實現對象:
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
//從反射工廠類中,顯示創建一個MethodAccessor實現對象
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
因為在通過反射獲取Method對象,在copy的時候,有個語句是res.root = this
,因為MethodAccessor是共享的,所以,可以先從父方法訪問器中獲取父類Method對象的methodAccessor屬性對象,判讀是否為null,不為null的話,直接返回。若是methodAccessor還是為空,則是需要顯示的調用ReflectionFactory.newMethodAccessor()方法。
(3)ReflectionFactory.newMethodAccessor()調用:
通過newMethodAccessor()方法,創建一個新的MethodAccessor對象。ReflectionFactory類中有幾個與MethodAccessor相關的屬性字段:noInflation,inflationThreshold。可以先看看創建新方法訪問對象的源代碼:
public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
//若是noInflation設置為true,則直接調用MethodAccessorGenerator創建新對象訪問對象
if (noInflation) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
從源代碼中可以看到有兩個分支:一個當設置noInflation屬性為true的時候,會直接調用MethodAccessorGenerator類直接創建新的MethodAccessor,內部的generatedMethod方法實現在稍后說明;另外一個分支中,這個分支也是常用的分支,因為noInflation默認設置為false。就來看看這個分支中是如何創建新的MethodAccessor對象的。
從源代碼中可以看到,主要的實現就是通過JNI存取器調用本地代碼庫NativeMethodAccessorImpl來創建對象訪問對象,當創建好對象后就通過DelegatingMethodAccessorImpl對象進行注入代理。
(4) NativeMethodAccessorImpl.invoke方法:
NativeMethodAccessorImpl類中會有一個字段numInvocations,用于計算使用JNI本地代碼來生成MethodAccessor對象的次數,當調用次數大于15次的時候,就會調用MethodAccessorGenerator.generateMethod生成類名字形如GeneratedMethodAccessorXXX的字節碼類文件。該類字節碼是通過DelegatingClassLoader來加載的。
為什么要設計這種機制呢?
java的反射機制運行效率是比較低的,執行Method.invoke()或Constructor.newInstance()都是通過調用native方法完成的,JDK為了提高反射運行效率,引入了一個機制叫“Inflation”,它首先通過DelegatingClassLoader去加載字節碼,再執行相關的邏輯,字節碼會緩存起來,所以第一次有加載的成本比正常執行慢3-4倍,但是后面的執行會有20倍以上的性能提升,這樣整體性能會有很大的提升。當然,這種機制也會有弊端,放在后面說。
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{ //大于15次創建GeneratedMethodAccessorXXX字節碼
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
//默認調用JNI存取器來返回一個MethodAccessor對象
return invoke0(method, obj, args);
}
(5) MethodAccessorGenerator.generateMethod方法調用:
當noInflation設置為true,即不使用Inflation機制,那么當Method對象的methodAccessor屬性為null的時候,就會直接調用MethodAccessorGenerator.generateMethod生成代理字節類GeneratedMethodAccessorXXX,并在內存中緩存起來;還要一種就是調用JNI的NativeMethodAccessorImpl.invoke次數大于15這個閾值的時候,也會調用這個方法生成代理字節類。
// MethodAccessorGenerator.java
private MagicAccessorImpl generate(final Class declaringClass,
String name,
Class[] parameterTypes,
Class returnType,
Class[] checkedExceptions,
int modifiers,
boolean isConstructor,
boolean forSerialization,
Class serializationTargetClass)
{
ByteVector vec = ByteVectorFactory.create();
asm = new ClassFileAssembler(vec);
this.declaringClass = declaringClass;
this.parameterTypes = parameterTypes;
this.returnType = returnType;
this.modifiers = modifiers;
this.isConstructor = isConstructor;
this.forSerialization = forSerialization;
//通過ClassFileAssembler類型的asm對象,設置Class字節碼內容,
//也就是通過代碼來構建一個符合JVM規范的Class字節碼文件
asm.emitMagicAndVersion();//設置魔數和版本.....
....
//這里有個重點,就是生成的字節碼文件的名字規則
//“GeneratedMethodAccessor+Num” NUm為調用次數
final String generatedName = generateName(isConstructor, forSerialization);
//通過ClassDefiner.defineClass方法,指定代理類加載器來加載這個類字節碼文件
//從而返回MethodAccessorImpl對象
return AccessController.doPrivileged(
new PrivilegedAction<MagicAccessorImpl>() {
public MagicAccessorImpl run() {
try {
return (MagicAccessorImpl)
//bytes是構建好的class字節碼文件
ClassDefiner.defineClass
(generatedName,
bytes,
0,
bytes.length,
declaringClass.getClassLoader()).newInstance();
} catch (InstantiationException e) {
throw (InternalError)
new InternalError().initCause(e);
} catch (IllegalAccessException e) {
throw (InternalError)
new InternalError().initCause(e);
}
}
});
}
//生成的class類字節碼文件的命名規則
private static synchronized String generateName(boolean isConstructor,
boolean forSerialization)
{
if (isConstructor) {
if (forSerialization) {
int num = ++serializationConstructorSymnum;
return "sun/reflect/GeneratedSerializationConstructorAccessor" + num;
} else {
int num = ++constructorSymnum;
return "sun/reflect/GeneratedConstructorAccessor" + num;
}
} else {
int num = ++methodSymnum;
return "sun/reflect/GeneratedMethodAccessor" + num;
}
}
可以看到這個generate方法主要做了一下幾件事情:
- 通過ClassFileAssembler類創建了符合JVM規范的MethodAccessor接口實現類的字節碼數組bytes。
- 通過generateName方法,生成class類字節碼文件:若是通過構造器反射調用,字節類文件名形如:"GeneratedConstructorAccessorXXXX",若是直接調用反射方法,不通過構造器,則如:"GeneratedMethodAccessorXXX"。
- 通過ClassDefiner.defineClass創建DelegatingClassLoader代理類加載器,用于加載上面生成的class字節碼,生成GeneratedMethodAccessorXXX類對象到內存中,以供Method.invoke方法調用。
當使用Method.invoke多次調用,生成GeneratedMethodAccessorXXX,并且使用DelegatingClassLoader加載該類的時候,通過DelegatedMethodAccessorImpl將GeneratedMethodAccessorXXX注入到目標方法的methodAccessor,所以實際也就是調用GeneratedMethodAccessorXXX.invoke()
方法,通過上圖代碼可知,也就是調用目標對象的方法,和正常的方法調用一樣。
(6) DelegatingClassLoader類加載器:
當通過MethodAccessorGenerator.generateMethod生成形如"GeneratedMethodAccessorXXX"的類字節對象,要加載到內存中使用,必須要通過類加載器來操作。那么,這些類字節文件就是被DelegatingClassLoader這個類加載器加載到內存中并進行裝配使用的。
//ClassDefiner.class
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);
}
}
加載到內存中后,就被緩存到java.lang.Class.class中的與反射有關的軟引用SoftReference給緩存起來,如:private volatile transient SoftReference<Method[]> declaredMethods。那么這個Inflation機制有哪些弊端呢?
Inflation機制提高了反射的性能,但是對于重度使用反射的項目可能存在隱患,它帶來了兩個問題:
(1)初次加載的性能損失;
(2)動態加載的字節碼導致PermGen持續增長;
當然了,解決方法也會有的,參照一下幾篇文章:
假笨說-從一起GC血案談到反射原理
使用反射代理類加載器的潛在內存使用問題
理解 JVM 如何使用 AIX 上的本機內存
sun.reflect.DelegatingClassLoader