JVM内存结构

概述

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的区域。在Java 7中,Java虚拟机所管理的内存如下图所示

Java 7

在Java 8中,内存结构发生了变化。主要变化是移除了方法区,新增了元数据区的概念。

Java 8

按线程划分

JVM

程序计数器(Program Counter Register)

  • 程序计数器是当前线程正在执行的字节码的地址。程序计数器是线程私有的,每一个线程都有一个独立的程序计数器。

  • 在Java虚拟机中,多线程是靠CPU时间片轮转来调度的,也就是说一个线程可能没有结束就被挂起。而他再次执行时,就要从挂起的地方继续执行。

  • 程序计数器是唯一一个不会出现OOM的区域。

虚拟机栈(Java stack)

  • 生命周期和线程相同,是线程私有的,目的是保证线程中的局部变量不会被别的线程放问到。
  • 每个方法从调用到执行完毕对应着一个栈帧的入栈出栈,所以栈帧不需要垃圾回收
  • 栈帧储存:局部变量、操作数栈、动态链接、方法出入口等信息。
  • StackOverflowError异常:线程请求的栈深度大于虚拟机允许的深度
  • OutOfMemoryError异常:虚拟机栈扩展时无法申请到足够的内存

本地方法栈(Native Method Stack)

  • 作用类似虚拟机栈,区别是本地方法栈为Native方法服务
  • 同样会抛出StackOverflowError异常和OutOfMemortError异常

堆(Heap)

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。对于大多数刚刚创建的Java应用俩说,如果运行卡顿,第一时间想到的可能是通过JVM挑优提高**-Xmx-Xms**的数值。

  • 所有线程共享的内存区域,虚拟机启动时创建,用来储存实例对象。
  • 可以是物理上不连续的内存空间。
  • 默认空间分配
    • 新生代(1/3的堆空间)
      • Eden区(8/10)
      • From区(1/10)
      • To区(1/10)
    • 老年代(2/3的堆空间)
  • OutOfMemoryError异常:无法满足内存分配需求且堆无法扩展时

方法区(Method Area)

概念

方法区(Method Area)是各个线程共享的内存区域,它用于储存已经被虚拟机加载的信息:

  • 非静态的属性
  • 非静态方法的元数据
  • 运行时常量池
  • 方法和构造函数编译后的代码
  • 类加载器初始化或者实例对象初始化用到的特殊方法

在理解它的构成之前,我们先了解一下两个概念:规范和实现。

针对Java虚拟机的实现有专门的《Java虚拟机规范》,在遵守规范的前提下,不同的厂商会对虚拟机进行不同的实现。就好比开发的过程中定义了接口,具体的接口实现大家可以根据不同的业务需求进行实现。

我们通常使用的Java SE都是Sun JDK和OpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM是HotSpot VM。通常情况下,我们讲的Java虚拟机指的就是HotSpot的版本。

虽然Java虚拟机规范将方法区描述为堆的逻辑部分,但是它却有一个别名叫做Non-堆(非堆),目的应该是将Java堆分开。

很多人把方法区叫做永久代(Permanent Generation)。这是因为在HotSpot中选择将GC分代手机扩展至方法区,或者说用永久代来实现方法区,实际上两者并不等价。将GC分代扩展至方法区后,HotSpot的垃圾收集器就可以像管理Java堆一样去管理这部分的内存,能够省区专门为方法区编写内存管理代码的工作。永久代和堆是相互隔离的,但它们使用的物理内存是连续的。永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集,这就产生了性能问题。而且常量池等一些类的信息也存在方法区内,而类及方法的信息难以确定它的大小。如果加载比较多的类很有可能造成内存溢出。而如果为方法区指定较大的内存的话,势必会压缩老年代内存空间,又容易造成老年代的溢出。

但在Java7中永久代中存储的部分数据已经开始转移到Java Heap或Native Momery中了。比如:符号引用(Symbols)转移到了Native Memory;字符串常量池(interned Strings)转移到了Java Heap;类的静态变量(class statics)转移到了Java Heap。

但在Java8已经取消了方法区这一个概念,而是用元空间来接管方法区的工作。

Java8改动

对于Java8,HotSopt取消了永久代,元空间登上舞台,方法区存在于元空间。同时,元空间不再和堆连续,而是存在于本地内存(Native Memory)。当Java Heap空间不足是会触发GC,但Native Memory空间不够时不会触发GC。

元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中OOM的错误。默认情况下元空间是可以无限使用本地内存的,但为了不让它膨胀,JVM同样提供了参数来限制它的使用。

元空间

元空间的特点

  • 充分利用了 Java 语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个加载器有专门的存储空间
  • 只进行线性分配
  • 不会单独回收某个类
  • 省掉了 GC 扫描及压缩的时间
  • 元空间里的对象的位置是固定的
  • 如果 GC 发现某个类加载器不再存活了,会把相关的空间整个回收掉

元空间的内存分配模型

  • 绝大多数的类元数据的空间都从本地内存中分配
  • 用来描述类元数据的类也被删除了
  • 分元数据分配了多个虚拟内存空间
  • 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型;sun / 反射 / 代理对应的类加载器的块会小一些
  • 归还内存块,释放内存块列表
  • 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
  • 减少碎片的策略

改动后的优势

如果你理解了元空间的概念,很容易发现 GC 的性能得到了提升。

  • Full GC 中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是 CMS 里面的那些)都删除了。
  • 元空间只有少量的指针指向 Java 堆。这包括:类的元数据中指向 java/lang/Class 实例的指针;数组类的元数据中,指向 java/lang/Class 集合的指针。
  • 没有元数据压缩的开销
  • 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
  • 减少了 Full GC 的时间
  • G1 回收器中,并发标记阶段完成后可以进行类的卸载
  • Hotspot 中的元数据现在存储到了元空间里。mmap 中的内存块的生命周期与类加载器的一致。
  • 类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
  • 可以进行参数的调优,不过这不是必需的。
  • 未来可能会增加其它的优化及新特性。比如, 应用程序类数据共享;新生代 GC 优化,G1 回收器进行类的回收;减少元数据的大小,以及 JVM 内部对象的内存占用量。

评论




Blog content follows the Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) License

载入天数...载入时分秒... Use Volantis as theme 鲁ICP备20003069号