Java的内存控制权在Java虚拟机。
运行时数据区域(内存模型)

虚拟机在执行Java程序的时候会把所管理的内存进行区域划分,初步分为两类:
- 运行时数据区域
- 本地内存
按照线程是否共享进行归类
线程私有
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享
- 堆
- 元空间
- 直接内存
程序计数器
- 程序计数器是当前线程执行字节码的行号指示器
- 通过改变计数器的值来选取下一条要执行的字节码指令(分支、循环、异常、线程恢复都依赖此)
- 每个线程都有一个独立的程序计数器,在阻塞恢复的过程中确保线程执行位置的准确,各线程间互不影响,独立存储
- 程序计数器是唯一一个不会出现OOM的内存区域
- 程序计数器的生命周期随着线程创建而创建,随着线程结束而死亡
Java虚拟机栈
- 线程私有,生命周期同线程
- 大部分Java方法的调用都是通过虚拟机栈以及配合其他区域来实现(除了一些Native方法通过本地方法栈)
- 方法调用的数据通过栈进行传递,调用时对应栈帧压入栈中,调用结束后被弹出
- 虚拟机栈是由一个个栈帧组成,先进后出
栈帧
栈帧结构分为四部分
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
局部变量表
- 存放编译器可知的各种数据类型
(boolean、byte、char、short、int、float、long、double)
- 存放对象引用 ?
(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
操作数栈
- 存放方法执行过程中产生的中间结果
- 存放执行过程中产生的临时变量
动态链接
用于方法调用其他方法的场景,作用时将方法的符号引用转为调用方法的直接引用。
- 在Java源文件编译为字节码时,所有的变量和方法引用都做为符号引用保存在Class文件中的常量池中
- 需要调用其他方法时,需要将常量池中指向方法的符号引用转为在内存地址中的直接引用
方法返回地址
虚拟机栈异常情况
Java方法有两种返回方式,一种return正常返回,一种抛出异常返回,无论哪种返回,都会弹出对应的栈帧。
- StackOverflowError:当栈空间不允许动态扩展时,当前线程请求栈的深度超过最大深度时,就会抛出错误
- OutOfMemoryError:当栈空间允许动态扩展时,如果在扩展的时候申请不到足够的内存空间,就会OOM
本地方法栈
- 和虚拟机栈一样
- 虚拟机栈服务Java方法,本地方法栈服务Native方法
- 也是由栈帧组成,也会出现两种错误
堆
- 虚拟机管理的最大一块内存
- Java中所有线程共享
- 虚拟机启动时创建
- 唯一目的就是存放对象实例,为对象实例、数组等分配内存
- 堆是垃圾回收器管理的主要区域,也叫GC堆
堆的划分
堆分为新生代、老年代,新生代分为Eden、Survivor、old
现在的垃圾回收期基本都是采用分代垃圾回收算法
详细的划区也是为了更好回收、分配内存

JDK8之后永久代被元空间替代,元空间使用直接内存
对象在堆中的变化
- 大部分情况,对象都会在Eden区进行内存分配
- 在一次新生代的垃圾回收后,如果对象还存活,就会进入S0或者S1区,并且对象年龄加1
- 当年龄增加到一定程度(默认15岁),就会晋升到老年代
堆中最容易出现OOM错误,有如下几种形式:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花太多时间执行垃圾回收并且只能回收很少内存时,会报错java.lang.OutOfMemoryError: Java heap space:创建新对象时,如果堆中空间不足,会报错(和分配的最大堆有关,最大堆受限于物理内存,最大为1/4)
方法区
- 逻辑区域
- 线程共享
- 方法区存储的是被加载的类信息、字段信息、方法信息、常量等数据
方法区和元空间的理解
方法区相当于规范,可以认为是方法区
元空间相当于实现,可以认为是类,实现接口的类
元空间
- 元空间使用的是直接内存,受本机内存限制,出现内存溢出的概率更小
- 元空间存放的是类的元数据
- 元空间需要指定大小,否则随着越来越多的类创建,虚拟机会耗尽系统内存
运行时常量池
- 常量池中存放的是编译器生成的字面量和符号引用
- 字面量包括整数、浮点数和字符串字面量
- 符号引用包括类、字段、方法、接口等符号引用
- 常量池属于方法区的一部分,当常量池无法申请到内存时就会OOM
字符串常量池
- 存在堆中
- 主要目的时为了避免字符串的重复创建
- 更高效进行垃圾回收