Java工程师面试1000题223-JDK1.8中JVM的变化:元空间的引入

223、JDK1.8中JVM的变化:元空间的引入

在介绍Java8中的JVM变化之前,我们先来回归一下Java的内存模型:

根据JVM规范,JVM内存共分为虚拟机栈、本地方法栈、堆、程序计数器、方法区五个部分:

1、虚拟机栈:每一个线程私有,线程之间彼此隔离,随着线程的创建而创建。栈里面存放的是一个叫做“栈帧”的东西,没去执行一个方法的时候就创建一个栈帧,栈帧里面存放的是局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等相关信息。栈的大小可以固定也可以动态扩展,当栈调用深度大于JVM所允许的范围时,会抛出StackOverflowError的错误。我们通过下面的程序测试一下栈调用的深度:

class Solution {

    private static int index = 1;

    public void call(){
        index++;
        call();
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        try {
            solution.call();
        }catch (Throwable e){
            System.out.println("栈深度为:" + index);
            e.printStackTrace();
        }
    }
}

执行多次的结果:

java.lang.StackOverflowError
栈深度为:24799
	at com.test.Solution.call(Solution.java:10)
java.lang.StackOverflowError
栈深度为:24827
	at com.test.Solution.call(Solution.java:10)
java.lang.StackOverflowError
栈深度为:24821
	at com.test.Solution.call(Solution.java:10)

发现每次执行的结果并不一样,就需要深入到 JVM 的源码中才能探讨,这里不作详细阐述。栈的高度称为栈的深度,栈深度受栈帧大小影响。局部变量表内容越多,栈帧越大,栈深度越小。虚拟机栈除了上述错误外,还有另一种错误,那就是当申请不到空间时,会抛出 OutOfMemoryError。这里有一个小细节需要注意,catch 捕获的是 Throwable,而不是 Exception。因为 StackOverflowError 和 OutOfMemoryError 都不属于 Exception 的子类。(线程私有)

2、本地方法栈:这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容。(线程私有)

3、程序计数器:JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。(线程私有)

4、堆:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。(线程共享)

5、方法区:方法区和上面的堆一样也是线程共享的。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了和堆进行区分,通常又叫做“非堆”。(线程共享)

注意:在JDK1.6及之前,运行时常量池是方法区的一个部分,同时方法区里面存储了类的元数据信息、静态变量、即时编译器编译后的代码(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)等。在JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。

在Java虚拟机规范中,方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择不在方法区实现垃圾回收与压缩。但是在HotSpot中,设计者将方法区纳入了GC分代收集。我们常说的永久代和方法区又是什么关系呢?《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的JVM上方法区的实现是不同的。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收。方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。

我们知道在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是major GC。

绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。PermGen(永久代),只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。

由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

至于JDK1.8中为什么要将永久代移除,大概有如下原因:有经验的同学会发现,对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。原因总结:

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

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

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

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

jdk1.8中虽然移除了永久代,但是仍然保留了方法区的概念,只不过实现方式不同。取消永久代,方法区存放于元空间中(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。变化如下:

1)移除了永久代(PermGen),替换为元空间(Metaspace);
2)永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
3)永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
4)永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)。

网上还有一种说法是:“JDK1.8时,移除了方法区的概念,用一个元数据区代替。元数据区存放的东西和方法区相同,不过元数据区移动到本地内存中。本地内存,又称堆外内存(Direct Memory),就是指机器内存中不是JVM管理的那部分内存,由操作系统管理。元数据区移动到本地内存以后,可以避免虚拟机加载类过多而引发的内存溢出:java.lang.OutOfMemoryError: PermGen,但是同样不能无限扩展。”个人觉得上面的说法不严谨,方法区是JVM的规范,不会被移除,只是实现形式由原来的永久代替换为了元空间而已。(个人愚见,欢迎大家纠正)

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

JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

猜你喜欢

转载自blog.csdn.net/qq_21583077/article/details/89210481