超详细JVM虚拟机内存区域详解 收藏 阅读:2320
2020-03-31 21:15:08

理解JVM虚拟机的内存划分,对开发或者面试都很重要的。包括理解内部结构、工作原理。

本篇会详细讲解jvm内存区域划分,并比对各个JDK版本之间差异。先看下图:

image-20200331102922044.png

Java运行时数据区一般我们分为五大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。它们的用途不同,有的是伴随着JVM启动而产生、有的是伴随着用户线程创建或销毁。

一. 程序计数器

或者叫做PC寄存器,PC,Program Counter Register。是线程私有的,每个线程都有自己的程序计数器。根据JVM规范,程序计数器用于记录当前线程执行JVM指令地址,包括用户定义的方法,或者本地方法(未指定值)。

  1. 线程上下文切换和程序计数器的关系?

    每个线程独有一份程序计数器,线程切换过程中,恢复线程执行的位置就是根据程序计数器完成的。从程序计数器中获取该线程需要执行的字节码的偏移地址。

  2. 线程执行本地方法和Java方法,程序计数器的异同?

    线程执行Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行 Navtive 方法,程序计数器值则为空(Undefined)。因为 Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。由于该方法是通过 C/C++ 而不是 Java 进行实现。那么自然无法产生相应的字节码,并且 C/C++ 执行时的内存分配是由自己语言决定的,而不是由 JVM 决定的。

  3. 程序计数器有没有可能发生OutOfMemoryError

    答案是不可能。在JVM规范中,唯一规定不发生OutOfMemoryError的区域。程序计数器保存的线程执行的偏移地址,再次执行,修改此偏移地址即可,不需要申请新的内存,所以不会发生。

二. Java虚拟机栈(Java Virtual Machine Stack)

早期的JVM叫做java栈。是线程私有,随着线程的创建而产生,线程结束而销毁。当线程创建时,会伴随创建栈帧(Stack Frame),对应着java方法的调用。

当java方法执行时,会创建栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息、方法正常退出或者异常退出的定义等信息。

一个方法调用的过程就对应着入栈(压栈)到出栈(弹栈)。

可以通过-Xss参数来设置虚拟机栈为固定大小,则当线程创建的时候,就会确定大小。也可以也允许通过计算结果动态来扩容和收缩大小。

会出现以下异常:

  1. 假如线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常。

  2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。

java虚拟机栈包括:

  1. 栈帧(Stack Frame)

    Java虚拟机栈的单位元素,每次方法调用都会创建。一个方法调用的过程就对应着入栈(压栈)到出栈(弹栈)。

  2. 局部变量表(Local Variable Table)

    栈帧中会存储局部变量表,包括参数和方法内部定义的局部变量。

  3. 操作数栈(Operand Stack)

    操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。

  4. 动态连接(Dynamic Linking)

    每个栈帧都包含一个指运行时常量池(JVM 运行时数据区域)中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。方法的重写,java中的多态,需要动态连接找到具体的方法去执行,JVM会通过多种方式优化查找过程,包括通过缓存。

  5. 方法返回地址

三. 堆(Heap)

堆是JVM最大的一块区域,是线程共享的区域,在JVM启动时创建,存储Java实例对象,通过-Xmx指定堆最大空间。

堆内存分为新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。

堆内存是逻辑上连续,物理上可以不连续的空间。堆空间可以动态分配,如果扩展无法申请足够内存,会抛出OutOfMemoryError

四. 本地方法栈(Native Method Stacks)

本地方法栈和Java 虚拟机栈发挥的作用类似,区别是,Java 虚拟机栈为运行Java方法服务,本地方法栈是为Native方法服务。在Hotspot JVM,两者是同一块区域。

五. 方法区(Method Area)

该区域被线程共享。早期的Jdk版本作用是类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。

现在JVM规范中,描述为Non-Heap(非堆),与堆做区分。包括运行时常量池(Runtime Constant Pool)。

运行时常量池(Runtime Constant Pool)内容包括:

  1. 字面量,包括:文本字符串、被声明为final的常量值、基本数据类型的值(基本数据类型的包装类比如Byte、Short、Integer、Long、Character、Boolean也实现了常量池技术,在-128到127之间用常量池)。Float 和 Double没有实现常量池技术。

  2. 符号引用,类和结构的完全限定名、字段名称和描述符、方法名称和描述符号。

方法区随着JDK的升级有如下变化:

1.JDK6

  • Klass 元数据信息

  • 每个类的运行时常量池(字段、方法、类、接口等符号引用)、编译后的代码

  • 静态字段(无论是否有final)在 instanceKlass 末尾(位于 PermGen 内)

  • oop(Ordinary Object Pointer(普通对象指针)) 其实就是 Class 对象实例

  • 全局字符串常量池 StringTable,本质上就是个 Hashtable

  • 符号引用(类型指针是 SymbolKlass)

2.JDK7

  • Klass 元数据信息

  • 每个类的运行时常量池(字段、方法、类、接口等符号引用)、编译后的代码

  • 静态字段从 instanceKlass 末尾移动到了 java.lang.Class 对象(oop)的末尾(位于 Java Heap 内)

  • oop 与全局字符串常量池移到 Java Heap 上

  • 符号引用被移动到 Native Heap 中

3.JDK8

  • 移除永久代

  • Klass 元数据信息

  • 每个类的运行时常量池、编译后的代码移到了另一块与堆不相连的本地内存 -- 元空间(Metaspace)

总结:有可能发生OutOfMemoryError错误区域如下:

  1. 堆内存不足,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”。

  2. Java虚拟机栈和本地方法栈,有可能出现StackOverFlowError和OutOfMemoryError。

  3. 老版本的JVM,堆内存的永久代,也会出现java.lang.OutOfMemoryError: PermGen space。随着方法区移入元数据区,异常信息变为:java.lang.OutOfMemoryError: Metaspace。


读后有收获,请作者喝杯咖啡


全部评论

发表评论