Metaspace 之二--PermGen vs. Metaspace 运行时比较

PermGen vs. Metaspace 运行时比较

  为了更好地理解Metaspace内存空间的运行时行为,

  将进行以下几种场景的测试:

  1. 使用JDK1.7运行Java程序,监控并耗尽默认设定的85MB大小的PermGen内存空间。

  2. 使用JDK1.8运行Java程序,监控新Metaspace内存空间的动态增长和垃圾回收过程。

  3. 使用JDK1.8运行Java程序,模拟耗尽通过“MaxMetaspaceSize”参数设定的128MB大小的Metaspace内存空间。

首先建立了一个模拟PermGen OOM的代码

public class ClassA {
    public void method(String name) {
    }
}

上面是一个简单的ClassA,把他编译成class字节码放到D:/classes下面,测试代码中用URLClassLoader来加载此类型上面类编译成class

package java8;

import java.io.File;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class OOMTest {
    public static void main(String[] args) {  
        try{  
            //准备url  
            URL url = new File("D:/classes").toURI().toURL();  
            URL[] urls = {url};  
            //获取有关类型加载的JMX接口  
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();  
            //用于缓存类加载器  
            List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();  
            while(true) {  
                //加载类型并缓存类加载器实例  
                ClassLoader classLoader = new URLClassLoader(urls);  
                classLoaders.add(classLoader);  
                classLoader.loadClass("ClassA");  
                //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)  
                System.out.println("total: "+ loadingBean.getTotalLoadedClassCount());  
                System.out.println("active: "+ loadingBean.getLoadedClassCount());  
                System.out.println("unloaded: "+ loadingBean.getUnloadedClassCount());  
            }  
        } catch(Exception e) {  
            e.printStackTrace();  
        }  
    }
}

虚拟机器参数设置如下:-verbose -verbose:gc

设置-verbose参数是为了获取类型加载和卸载的信息

设置-verbose:gc是为了获取垃圾收集的相关信息

 

JDK 1.7 @64-bit – PermGen 耗尽测试

Java1.7的PermGen默认空间为85 MB(或者可以通过-XX:MaxPermSize=XXXm指定)

可以从上面的JVisualVM的截图看出:当加载超过6万个类之后,PermGen被耗尽。我们也能通过程序和GC的输出观察耗尽的过程。

程序输出(摘取了部分)

  1. ......  
  2. [Loaded ClassA from file:/D:/classes/]  
  3. total: 64887 
  4. active: 64887 
  5. unloaded: 
  6. [GC 245041K->213978K(536768K), 0.0597188 secs]  
  7. [Full GC 213978K->211425K(644992K), 0.6456638 secs]  
  8. [GC 211425K->211425K(656448K), 0.0086696 secs]  
  9. [Full GC 211425K->211411K(731008K), 0.6924754 secs]  
  10. [GC 211411K->211411K(726528K), 0.0088992 secs]  
  11. ...............  
  12. java.lang.OutOfMemoryError: PermGen space 

JDK 1.8 @64-bit – Metaspace大小动态调整测试

Java的Metaspace空间:不受限制 (默认)

从上面的截图可以看到,JVM Metaspace进行了动态扩展,本地内存的使用由20MB增长到646MB,以满足程序中不断增长的类数据内存占用需求。我们也能观察到JVM的垃圾回收事件—试图销毁僵死的类或类加载器对象。但是,由于我们程序的泄漏,JVM别无选择只能动态扩展Metaspace内存空间。程序加载超过10万个类,而没有出现OOM事件。

JDK 1.8 @64-bit – Metaspace 受限测试

Java的Metaspace空间:128MB(-XX:MaxMetaspaceSize=128m)

可以从上面的JVisualVM的截图看出:当加载超过2万个类之后,Metaspace被耗尽;与JDK1.7运行时非常相似。我们也能通过程序和GC的输出观察耗尽的过程。另一个有趣的现象是,保留的原生内存占用量是设定的最大大小两倍之多。这可能表明,如果可能的话,可微调元空间容量大小策略,来避免本地内存的浪费。

从Java程序的输出中看到如下异常。

  1. [Loaded ClassA from file:/D:/classes/]  
  2. total: 21393 
  3. active: 21393 
  4. unloaded: 
  5. [GC (Metadata GC Threshold) 64306K->57010K(111616K), 0.0145502 secs]  
  6. [Full GC (Metadata GC Threshold) 57010K->56810K(122368K), 0.1068084 secs]  
  7. java.lang.OutOfMemoryError: Metaspace 

在设置了MaxMetaspaceSize的情况下,该空间的内存仍然会耗尽,进而引发“java.lang.OutOfMemoryError: Metadata space”错误。因为类加载器的泄漏仍然存在,而通常Java又不希望无限制地消耗本机内存,因此设置一个类似于MaxPermSize的限制看起来也是合理的。

总结

  1. 之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
  2. 提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;
  3. 隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。

 

总结

  • Hotspot中的元数据现在存储到了元空间里。mmap中的内存块的生命周期与类加载器的一致。
  • 类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
  • 可以进行参数的调优,不过这不是必需的。
  • 未来可能会增加其它的优化及新特性。比如, 应用程序类数据共享;新生代GC优化,G1回收器进行类的回收;减少元数据的大小,以及JVM内部对象的内存占用量。
posted on 2014-03-20 15:54  duanxz  阅读(1338)  评论(0编辑  收藏  举报