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

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

pod内存和java内存 java中内存的四个区_Java

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

一. 程序计数器

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

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

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

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

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

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

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

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

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

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

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

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

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

java虚拟机栈包括:栈帧(Stack Frame)

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

局部变量表(Local Variable Table)

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

操作数栈(Operand Stack)

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

动态连接(Dynamic Linking)

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

方法返回地址

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

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

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

1.JDK6Klass 元数据信息

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

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

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

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

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

2.JDK7Klass 元数据信息

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

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

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

符号引用被移动到 Native Heap 中

3.JDK8移除永久代

Klass 元数据信息

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

总结:有可能发生OutOfMemoryError错误区域如下:堆内存不足,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”。

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