深入解释Java7中运行UT的OutOfMemoryError: PermGen space

来源:互联网 发布:java 进程创建原语 编辑:程序博客网 时间:2024/06/05 10:30

问题描述

在Java 8被广泛应用以前,java程序员一定在某个时间点不期而遇过OutOfMemoryError: PermGen space的问题。这个异常打印已经把描述得很清楚,内存中永久代的空间不足。今天我也遇到了这个问题,或许这个问题还比较典型:一组UT代码,在经过一段时间的修改和膨胀之后,“突然”某一天在运行的时候抛出了OutOfMemoryError: PermGen space异常,导致test suite无法正常运行。经过分析,直接原因是因为使用了太多由PowerMock或Mockito mock出来的类,这些类的元文件信息大量的挤占了PermGen区的内存,导致内存不够。具体是怎么样形成的呢?我们先看看背后的知识原理。

Java的内存模型

简单说下,JVM 内存包含如下几个部分:

  • 堆内存(Heap Memory): 存放Java对象
  • 非堆内存(Non-Heap Memory): 存放类加载信息(class)和其它meta-data
  • 其它(Other): 存放JVM 自身代码等

在Java虚拟机(JVM)内部,class文件中包括类的版本、字段、方法、接口等描述信息,还有运行时常量池,用于存放编译器生成的各种字面量和符号引用。

在过去(自定义类加载器还不是很常见的时候),类大多是”static”的,很少被卸载或收集,因此被称为“永久的(Permanent)”。同时,由于类class是JVM实现的一部分,并不是由应用创建的,所以又被认为是“非堆(non-heap)”内存。

在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM由于指针膨胀,默认是85M)。永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OutOfMemoryError。

注:在JDK7之前的版本,对于HopSpot JVM,interned-strings存储在永久代(又名PermGen),会导致大量的性能问题和OOM错误。从PermGen移除interned strings的更多信息查看这里。

因此,在我们研究OutOfMemoryError: PermGen space的时候,主要考虑的是类加载超限的问题。

Class在PermGen中的存储

以下是PermGen中主要存储的内容:

  • 类的基本字段
  • 类的方法(Methods,包括方法的bytecodes)
  • 所有类的名字
  • 以对象形式指向的字符串
  • 常量池信息(数据由class文件读入到PermGen)
  • JVM创建的内部的对象
  • Object arrays and type arrays associated with a class (e.g., an object array containing references to methods).
  • 用于优化编译的各种信息(JITs)

如果没有一个直观的认识,或许你不会意识到一个class涉及到的信息有多大。

单纯的Junit Case

这是一个仅仅通过Junit运行了一个什么都没有测的用例。

public class TestPermGen{    @Before    public void setUp()    {        BufferedReader bufferedReader =            new BufferedReader( new InputStreamReader( new BufferedInputStream( System.in ) ) );        try        {            bufferedReader.readLine();        }        catch( IOException e )        {            e.printStackTrace();        }    }    @Test    public void nothing()    {        return;    }}

我们用Jprofiler来监控,运行时需要消耗的PermGen内存:
这里写图片描述
单单是加载Junit的框架和相关的类,已经达到了7MB的使用量。不过如果仅仅是Junit,那你运行再多的Test case,相关的类也只会被加载一次。
但如果我们用了Mockito去Mock了一些比较复杂的类,结果会如何?

使用了Mockito的Test case

public class TestPermGen{    @Mock    private WorkItemImpl workItem;    @Mock    private EJBProvider ejbProviderMock;    @Mock    private WorkItemManager workItemManager;    @Mock    private ServiceDispatcher serviceDispatcherMock;    @Before    public void setUp()

这里写图片描述

4个类,将近300KB的内存。因为这些类是RunTime生成并且载入到内存当中的,因此每个Test Case Class会拥有自己的inner mock class:
这里写图片描述
因为Junit在运行时是以整个Test Suite作为一个JVM进程在运行,所以在你的Test Suite中,每个Test case mock的东西越多,整个Test Suite使用的内存就越多。

使用了MockitoJunitRunner的Test case

@RunWith( MockitoJUnitRunner.class )public class TestPermGen{    @Mock    private WorkItemImpl workItem;    @Mock    private EJBProvider ejbProviderMock;    @Mock    private WorkItemManager workItemManager;    @Mock    private ServiceDispatcher serviceDispatcherMock;

内存疯长。这里,仍然是不同的Class,既类似的Case越多,内存越多。

这里写图片描述

0 0
原创粉丝点击