Java 内存区域的简单介绍

115 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

0 前言

Java 程序员在开发过程中,不必去编写实例对应的释放内存的操作,Java 程序在运行期间,并不容易出现内存溢出的问题,这得益于 Java 虚拟机的自动内存管理机制。但是一旦出现了内存泄露、溢出问题,如果不了解虚拟机是如何使用内存的,就不好排查问题。

因此,我们有必要了解 Java 虚拟机的内存区域及各自的作用。

Java 虚拟机的在 Java 程序运行过程中,会为它所管理的内存划分出五个数据区域。

未命名文件 (3).png

1 程序计数器

程序计数器(Program Counter Register)的主要作用是记录下一条待执行的 Java 字节码指令,字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令,如分支、循环、跳转、异常处理、线程恢复等程序流程都依赖程序计数器来完成。

每个线程都有自己的程序计数器,互不影响,也正因如此,线程恢复运行后才能继续正常执行。

程序计数器是《Java虚拟机规范》中唯一一个没有规定 OutOfMemoryError 情况的区域,这意味着内存溢出问题不会是程序计数器引起的。

2 Java虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,生命周期与线程相同。

它描述的是 Java 方法执行的内存模型,一个虚拟机栈对应一个线程所占用的内存空间。

Java 方法执行时,会创建一个“栈帧”,将其加入到虚拟机栈中,方法执行结束后,“栈帧”出栈并释放内存。即虚拟机栈中的栈帧入栈出栈操作,对应着一个方法从调用到执行完毕的过程。

栈帧用于存储:

  • 局部变量表:可以理解为一个数组,局部变量的获取与设置都在这个数组上执行

    • 每个“变量槽位”可以存放一个:boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据
    • reference 表示一个对象实例的引用
    • returnAddress 指向一条字节码指令的地址,可以实现异常处理时的跳转
  • 操作数栈:在对变量执行操作时,一般都会将变量压入操作数栈,字节码指令通过操作栈顶元素来完成功能。

  • 动态连接:将符号引用转化为直接引用,指向内存中真正的对象实例。

  • 方法出口:记录方法被调用时的位置,让程序继续运行。

垃圾回收不会管理栈内存,因为栈帧所占用的内存随着方法调用结束而释放。

虚拟机栈会出现内存溢出问题:

  • StackOverflowError:例如递归调用导致,虚拟机栈无法再压入栈帧
  • OutOfMemoryError:创建新的线程,创建新的虚拟机栈,内存没有足够的空间分配虚拟机栈

3 本地方法栈

作用与虚拟机栈类似,区别是本地方法栈描述的是操作系统本地方法执行的内存模型。

本地方法栈也会出现StackOverflowErrorOutOfMemoryError

4 Java 堆

堆是 Java 内存区域最大的一块内存区域,在虚拟机启动时创建,被所有线程共享。

Java 堆用于存放对象实例,受垃圾回收管理。

在 Java 堆上又可以继续细分:

  • 新生代:用于存放刚创建的、随时会被回收的对象实例

    • Eden:新创建的对象实例存放于该区域
    • From Survivor:上次垃圾回收后存活的对象实例存放于该区域
    • To Survivor:垃圾回收后存放存活的实例,随后变更为 To Survivor, From Survivor 变更为 To Survivor
  • 老年代:用于存放存活时间长的实例或大对象

堆内存会出现内存溢出错误:OutOfMemoryError

5 方法区

方法区(Method Area)也是各个线程共享的内存区域,同样受垃圾回收管理。

主要用于存储虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等数据。

在 JDK8 之前,方法区的实现是“永久代”,使用的是 JVM 的内存。

在 JDK8 及之后,方法区的实现是“元空间”,使用的是操作系统的本地内存。

同样,当方法区无法满足新内存分配时,也会抛出OutOfMemoryError

运行时常量池是方法区的一部分。

类的二进制字节码文件包含三类基本信息:

  • 类的基本信息
  • 常量池:存放各种字面量与符号引用
  • 类方法定义,包含了虚拟机指令

当类被加载到虚拟机中时,类的常量池也会加载到运行时常量池。