对于JVM内存模型的理解(对比jdk1.7与1.8)

在这里插入图片描述
类的元数据信息转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。

此外,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。

JDK1.7内存模型

对于这五个区域我大概做一个大概介绍,详细情况可以查阅其他资料或者文章

程序计数器:线程私有,可以看做当前程序执行的行号指令器。

Java虚拟机栈:线程私有,生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行时会形成一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,一个方法从调用到执行完毕,就是一个栈帧从进栈到出栈的过程,下图是虚拟机栈的结构模型,图片来源和详细解释:https://www.cnblogs.com/aflyun/p/10575740.html

 本地方法栈:线程私有,作用于Java虚拟机栈类似,只不过Java虚拟机栈执行Java方法,而本地方法栈运行本地的Native方法。

:Java虚拟机管理的最大的一块内存区域,Java堆是线程共享的,用于存放对象实例。也就是说对象的出生和回收都是在这个区域进行的。堆分为初生代(Young Gen)和老年代(Tenured Gen),比例默认为1:2,而初生代又分为Eden和From和To三个区域,比例默认为8:1:1

方法区:线程共享,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,这里要说一下方法区和永久代到底是什么关系,永久代是HotSpot虚拟机对于方法区的实现,方法区的实现是不受虚拟机规范约束的,这里只是HotSpot虚拟机团队是这样实现的。

运行时常量池:在JDK1.7中,是运行时常量池是方法区的一部分,用于存放编译期生成的各种字符变量和符号引用。其实除了运行时常量池,还有字符串常量池,class常量池,具体的介绍和区别请看这篇文章:Java中的常量池(字符串常量池、class常量池和运行时常量池)

以上就是JDK1.7的虚拟机内存模型。

JDK1.8内存模型

JDK1.8与1.7最大的区别是1.8将永久代取消,取而代之的是元空间,既然方法区是由永久代实现的,取消了永久代,那么方法区由谁来实现呢,在1.8中方法区是由元空间来实现,所以原来属于方法区的运行时常量池就属于元空间了。元空间属于本地内存,所以元空间的大小仅受本地内存限制,但是可以通过-XX:MaxMetaspaceSize进行增长上限的最大值设置,默认值为4G,元空间的初始空间大小可以通过-XX:MetaspaceSize进行设置,默认值为20.8M,还有一些其他参数可以进行设置,元空间大小会自动进行调整,详细请看这篇文章:jdk8 Metaspace 调优

这里要说明一下,要区分字符串常量池和运行时常量池,这里引用这篇文章JDK1.8关于运行时常量池, 字符串常量池的要点所提到的:

  •  在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  • 在JDK1.7字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  • 在JDK1.8hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace) 

 

 

从jdk开始,就开始了永久代的转移工作,将譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但是永久在还存在于JDK7中,直到JDK8,永久代才完全消失,转而使用元空间。而元空间是直接存在内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。

为什么叫元空间,是因为这里面存储的是类的元数据信息

元数据(Meta Date),关于数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。
       这些定义都很是抽象,我们可以把元数据简单的理解成,最小的数据单位。元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)

为什么这么做呢?

类的元数据, 字符串池, 类的静态变量将会从永久代移除, 放入Java heap或者native memory. 其中建议JVM的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入Java堆中. 这样可以加载多少类的元数据就不在由MaxPermSize控制, 而由系统的实际可用空间来控制.

为什么这么做呢? 减少OOM只是表因, 更深层的原因还是要合并HotSpot和JRockit的代码, JRockit从来没有一个叫永久代的东西, 但是运行良好, 也不需要开发运维人员设置这么一个永久代的大小.


5、方法区:

方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。 关于方法区内存溢出的问题会在下文中详细探讨。

二、PermGen(永久代)

绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:

本例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 "java.lang.OutOfMemoryError: PermGen space " 异常了。这里之所以采用 JDK 1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。

三、Metaspace(元空间)

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

在这里插入图片描述
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

四、总结

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

4、Oracle 可能会将HotSpot 与 JRockit 合二为一。