JVM高阶面试:Java8为什么使用元空间替换永久代?

1. 方法区简介

JVM 的内存模型主要包括程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、堆(Heap)和方法区(Method Area)。

方法区(Method Area)是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

具体来说,方法区用来存储以下数据:

  1. 类的元数据信息:包括类的名称、访问标志、父类、接口、字段、方法等信息。
  2. 运行时常量池:在Java代码中,常量可以被直接定义在类或接口中,这些常量在编译后被存储在Class文件的常量池中,而运行时常量池则是从Class文件中加载的。
  3. 静态变量和常量:类的静态变量和常量都存储在方法区中,它们在类加载的时候被初始化并分配内存空间。
  4. 方法字节码:在Java中,方法的字节码被编译成Class文件并存储在方法区中。
  5. 即时编译器(JIT)编译后的代码:为了提高程序的执行效率,JIT会将热点代码编译成本地机器码并存储在方法区中。

方法区只是 JVM 规范中定义的一个概念,针对 Hotspot 虚拟机,Java8 之前使用永久代(Permanent Generation,简称 PermGen)实现,而 Java8 之后使用元空间(Metaspace)实现。

JDK8 之前可以通过 -XX:PermSize 和 -XX:MaxPermSize 来设置永久代大小,JDK8 之后,使用元空间替换了永久代,改为通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间大小。

JVM高阶面试:Java8为什么使用元空间替换永久代?

2. 永久代问题

2.1 内存溢出

永久代的空间是有限制的,可以通过 -XX:PermSize 设置永久代初始容量,通过-XX:MaxPermSize 设置永久代最大容量。

但是当加载过多的类或者常量的时候,就可能导致永久代的空间不足,抛出 java.lang.OutOfMemoryError: PermGen space 异常。尤其是web应用会使用很多框架,这些框架会动态加载很多基础类,更容易导致OOM。

2.2 垃圾回收效率低下

永久代中的类信息一般是在应用程序运行期间不会发生变化的,因此,如果开启了永久代的垃圾回收,就会造成大量的垃圾回收操作,导致垃圾回收效率低下,甚至会引起应用程序的暂停。

此外,由于永久代主要存放 JVM 加载的类信息等永久存在的数据,这使得它在垃圾回收过程中的回收效率相对较低。在某些情况下,频繁触发的 Full GC 不仅无法有效回收永久代空间,还会严重影响 JVM 的性能。

2.3 无法动态调整大小

永久代的大小一旦被设置,就无法动态调整,如果预估错误,就可能导致浪费内存或内存不足的问题。

2.4 无法回收常量池中的内存

在永久代中,常量池是一个非常重要的部分,但是其中的常量无法被回收,即使这些常量已经不再被使用,也无法被垃圾回收器回收,这会浪费内存。

3. 元空间简介

元空间(Metaspace)是 Java8 中引入的一个新概念,用来替代原来的永久代。与永久代不同,元空间并不在虚拟机中,而是存储在本地内存(Native Memory)。

从 Java7 已经开始逐步移除永久代,在Java7中把 interned Strings 、 class statics 和 String Pool 从永久代移到堆中。在 Java8 中彻底移除了永久代,把将类的元数据信息、常量、静态变量、即时编译器编译后的代码从永久代中移到了元空间中。

JVM高阶面试:Java8为什么使用元空间替换永久代?

4. 元空间的优点

与永久代相比,使用元空间使用方法区具有以下优点:

  • 突破内存限制,减少OOM。 由于元空间使用的是本地内存,而不是 JVM 内存,因此理论上,其大小只受限于操作系统的实际可用内存。这大大减少了内存溢出的可能性。相较于永久代在 JVM 堆中预分配的有限空间,元空间的引入提供了更大的空间来存储类元数据。
  • 提高 Full GC 的效率。 在永久代中,Full GC 的触发比较频繁,而且效率较低。因为永久代中存放了很多 JVM 需要的类信息,这些数据大多数是不会被清理的,所以 Full GC 往往无法回收多少空间。但在元空间模型中,由于字符串常量池已移至堆中,静态变量也移至 Java 堆或者本地内存,因此可以更有效地进行垃圾回收,避免了因频繁的 Full GC 导致的性能影响。
  • 满足不同的类加载需求和动态类加载的情况。 在一些大型的、模块化的应用中,可能需要加载大量的类,这就需要大量的元数据存储空间。元空间可以动态地调整大小,能更好地满足这种需求。
  • 避免永久代调优和大小设置的复杂性。 在 Java8 之前的版本中,通常需要手动设置永久代的大小,以避免内存溢出的错误。这增加了应用的配置和管理的复杂性。而元空间使用本地内存,根据实际需求动态调整,大大简化了内存管理的复杂性。

5. 元空间问题

尽管元空间解决了永久代的一些问题,可能也同时引入了一些新问题:

  • 可能导致本地内存溢出:虽然元空间使用的是本地内存,理论上其大小只受限于操作系统的实际可用内存,但是如果元空间的使用不加以控制,可能会导致大量的本地内存被占用,从而导致 OutOfMemoryError。
  • 内存管理和调优策略:永久代的内存管理和调优策略无法直接应用到元空间,需要重新考虑和设计。例如,如何确定元空间的初始大小、最大大小,如何进行垃圾回收,等等。

因此,虽然元空间为 JVM 的内存管理带来了新的可能,但也带来了新的挑战。为了充分利用元空间的优势,开发者需要理解其工作原理,掌握正确的使用和调优方法。

6. 总结

Java8 选择使用元空间(Metaspace)替代永久代(PermGen)是 JVM 内存模型的一次重大改进。解决了永久代面临的空间限制、低效的垃圾回收、以及复杂的内存管理等问题。元空间利用本地内存,能够动态调整大小,提供了更大的空间来存储类元数据,也更好地适应了大型、模块化应用的需求。

但是元空间也引入了一些新问题。如何避免本地内存溢出,如何制定有效的内存管理和调优策略,都是开发者需要重新考虑的问题。

© 版权声明

相关文章