反射如何影响Perm大小?

9

我理解perm size用于存储元数据,包括字节码、静态内容等。

我的问题是:反射的使用会如何影响perm size?如果有影响的话。我的意思是,如果程序A使用正常的对象运行方式,而程序B则大量使用反射,那么这两个程序的perm size会有什么区别

4个回答

7

在执行加载新类或内部化字符串的代码时,perm空间将增长。反射类必须被加载,这是确定的。我不确定反射API是否会大量使用内部化字符串,但应该很容易找出。

例如,对于方法getDeclaredMethod(String name, Class<?>... parameterTypes),名称参数将被内部化。

以下代码片段将填充perm空间:

Random random = new Random();
while(true){
    try {
        X.class.getDeclaredMethod(toBinaryString(random.nextLong()));
    } catch (Exception e) {
    }
}

alt text http://img4.imageshack.us/img4/9725/permgen.jpg

在实际应用中,这不会有任何区别。


我认为你看到的是将搜索的名称进行内部化,以便可以在已经内部化的方法名称上使用==。 (并不确定这是一个好主意。)实际上,反射可以创建类(有时)。 - Tom Hawtin - tackline
@Tom,你能举一个通过反射创建类的例子吗?我不是想刁难你,我真的很想知道这些情况,因为我从未见过这样的例子。 - PSpeed
"interned string"... Java 会保留字符串对象的缓存。当一个类被加载时,该类定义中的字符串(引用其他类名、类使用的字符串常量等)将被添加到 intern 缓存中。String.intern() 是直接访问此缓存的方法。该方法的 javadocs 进一步解释了这一点。 - PSpeed
上面的片段会占用perm空间,因为Class.getDeclaredMethod()在方法搜索期间将传递的字符串intern()。这对于正常使用来说是可以接受的,并允许它们在内部使用== ...但在像上面那样调用大量错误方法名称的退化情况中,它会用大量垃圾填充intern缓存。 - PSpeed
Jaxb问题本身似乎很重要,但与反射无关。我从未经常使用Jaxb,因此无法评论他们为什么会这样做,但有许多缓存访问的策略。您引用的链接表明,原帖发布者通过不断重新初始化整个环境来错误地使用了Jaxb。 - PSpeed
显示剩余9条评论

3

我不完全确定我理解了这个问题,但Java类已经包含了执行反射所需的一切。一个.class文件不会根据它的使用方式而改变。

编辑1:提供一些更具体的示例,以更好地区分我们所讨论的“反射”类型。

示例:

class Foo {
    public void bar() {
        System.out.println( "Hello, world." );
    }
}

// Conventional calling
Foo f = new Foo();
foo.bar();

// Reflection based callling
Class fooClass = Class.forName( "Foo" );
Method m = fooClass.getMethod( "bar", null );
Object f = fooClass.newInstance();
m.invoke( m, null );     

在传统的调用情况下,将加载Foo.class及其所有直接类依赖项。bar将被执行。字符串“Foo”和“bar”已作为字节码在运行时使用字符串来解析类和方法而被interned为调用类的一部分。(实际上,“bar”将是完整的方法签名,因此比“bar”更长)
在反射情况下,完全相同的事情发生。唯一额外加载的类是Method.class。这应该是perm大小的唯一影响。
后一种情况具有性能影响。方法查找相对昂贵,因此建议尽可能缓存Method对象。调用的额外方法调用具有轻微的性能影响,因为它是额外的方法调用。Hotspot将难以通过此调用进行优化...至少比正常情况更难。JITing完全相同。
编辑2:注意一些额外的对象在反射期间加载...
java.lang.Class将在访问时创建并缓存Method对象(或Field对象等)。这些被缓存在SoftReference中,因此如果内存使用需要,它们将被回收。
但是,这些对象的初始化意味着VM可能会加载其他interned字符串以支持创建这些Method对象。我猜测这些字符串可能已经是反射类的常量池的一部分,但也有可能不是。无论哪种方式,每个方法每个类,每个字段每个类,都是单次命中。访问方法,您将至少获得所有这些方法的名称interned。访问字段,您将获得这些字段的名称interned。

类可以通过反射实现动态生成,以实现快速访问。 - Tom Hawtin - tackline
请问有参考资料吗?我经常使用反射(真的...非常频繁),但从未见过因此而生成单个类。第三方库可能会执行一些操作,动态代理肯定会生成运行时类。但仅仅调用类/对象上的方法不应该生成类。 - PSpeed
@PSpeed,请查看以下链接:https://jaxb.dev.java.net/issues/show_bug.cgi?id=581 - Suraj Chandran
@Tom...你能给个例子吗?那正是我正在寻找的! - Suraj Chandran

3

反射可以生成类,具体取决于实现方式。例如,在编译为字节码之前,您可能需要使用反射工件数千次。

一如既往的建议是:在实际情况下对您的代码进行分析。


你是指上面的“本地代码”吗?这些类已经是字节码了。Hotspot在通过反射调用时优化会有困难,但JIT的行为是相同的。 - PSpeed
不,我指的是字节码。显然,在使用HotSpot后,字节码将被编译为本机代码。可以通过从对象开头找到偏移量并添加来读取字段。它们也可以通过编写读取这些字段的字节码(忽略访问权限)来读取。在Sun JRE中会发生类似的事情,但我忘记了细节。 - Tom Hawtin - tackline
+1:只需激活GC日志并检查System.out - [卸载类sun.reflect.GeneratedMethodAccessor1403] [卸载类sun.reflect.GeneratedConstructorAccessor446] [卸载类sun.reflect.GeneratedMethodAccessor562] - 这些是反射实现生成的。 - fglez

2
第一个答案基本上是正确的 - 无论是正常加载类还是通过反射加载,都没有什么区别。无论哪种方式,都会被加载。我不知道任何关于字符串常量池方面的区别。
我想有一个更间接的影响:使用反射,你可能会导致较少的类被加载。假设你有一些代码根据某个参数加载100个类中的其中一个。如果没有使用反射,你需要导入所有100个类并实例化其中一个。
使用反射,只有1个类会被加载。没有使用反射,在此代码加载的时候,所有100个导入的类都会被加载,因为它们都被引用。因此,更多的类会被加载到perm空间中。
但这是一个有点牵强的例子。除非它真正描述了你的情况,否则你几乎不会注意到任何区别。也就是说,直到你确定它不是微不足道的,不要让它影响你的设计决策。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接